How to easily add a property to a function in TS code

This article was published on Dec 12, 2022, and takes approximately a minute to read.

In React world, there's a famous pattern for components libraries while exporting components with subcomponents bound, for example:

import { Menu } from "@company/components";

export function App() {
  return (
    <div>
      <Menu>
        <Menu.Item>Item 1</Menu.Item>
        <Menu.Item>Item 1</Menu.Item>
      </Menu>
    </div>
  );
}

As you can see, Menu is a component that can be used and invoked, and it has a component called `Item` inside it that can also be invoked.

There's an old common sense that "everything in JavaScript is an object", which is not 100% accurate (because of what is the primitives), but most of the time is true.

So, to achieve such a thing, we would have a code like this:

Menu.jsx
// Declare the Menu Component and export it
export function Menu({ children }) {
  return <ul className="menu">{children}</ul>;
}

// Declare the MenuItem Component
function MenuItem({ children }) {
  return <li className="menu-item">{children}</li>;
}

// Assign the MenuItem Component to the Menu Component
Menu.MenuItem = MenuItem;

Now you can use the Menu component as shown in the first example.

In old TypeScript versions (also depending on your config), you might have problems where TS says, "MenuItem is not present on Menu".

To solve this problem, you could use the native method Object.assign:

Menu.tsx
import React from "react";

function BaseMenu({ children }: { children: React.ReactNode }) {
  return <ul className="menu">{children}</ul>;
}

function MenuItem({ children }: { children: React.ReactNode }) {
  return <li className="menu-item">{children}</li>;
}

export const Menu = Object.assign(BaseMenu, { MenuItem });

By doing this, TS will understand that we're binding MenuItem within the Menu component, and the type inference will work just fine.