How to Type Custom Components with TypeScript in Material-UI: A Complete Guide to Polymorphic Components

Material-UI provides a polymorphic typing system centered around the OverridableComponent interface that enables you to type custom components with TypeScript while maintaining full type safety when overriding the root element via the component prop.

When you type custom components with TypeScript in Material-UI, you leverage the same polymorphic architecture that powers every component in the library. The mui/material-ui repository implements this system through the OverridableComponent interface and auxiliary type helpers located in packages/mui-material/src/OverridableComponent/index.ts. These utilities ensure that props like component, ref, and sx remain fully typed regardless of which root element you render, providing autocomplete and compile-time validation across the entire component tree.

Understanding the Polymorphic Type System

Material-UI's type system is built on the concept of polymorphic components—components that can render as different root elements while preserving type safety. The foundation of this system is the OverridableComponent interface, which lives in packages/mui-material/src/OverridableComponent/index.ts.

This interface uses TypeScript function overloads to provide two distinct call signatures:

  • One signature for when you supply a component prop to override the default root.
  • One signature for when you use the component with its default root element.

The system relies on several interconnected types defined in the same file:

Type Purpose Location
OverridableComponent<TypeMap> Declares a component with swappable root elements using function overloads. packages/mui-material/src/OverridableComponent/index.ts
OverrideProps<TypeMap, RootComponent> Computes props when component={RootComponent} is used, merging component props with root props while excluding duplicates. packages/mui-material/src/OverridableComponent/index.ts
DefaultComponentProps<TypeMap> Props available when using the default root element (no component prop). packages/mui-material/src/OverridableComponent/index.ts
BaseProps<TypeMap> Union of component-specific props and generic Material-UI props (CommonProps). packages/mui-material/src/OverridableComponent/index.ts
CommonProps Shared props like className, style, and sx. packages/mui-material/src/OverridableComponent/index.ts
OverridableTypeMap Shape required for a component's type map, defining props and defaultComponent. packages/mui-material/src/OverridableComponent/index.ts

Step-by-Step Guide to Type Custom Components with TypeScript

To properly type custom components with TypeScript in Material-UI, you follow a three-step pattern that mirrors how the library's own components are built. This approach ensures your components support the component prop, forward refs correctly, and maintain full type safety.

Step 1: Define the TypeMap

First, create a TypeMap interface that describes your component's props and its default root element. This interface must conform to OverridableTypeMap.

import * as React from 'react';

interface MyButtonTypeMap<P = {}, D extends React.ElementType = 'button'> {
  props: P & {
    /** Custom color variant */
    myColor?: 'primary' | 'secondary';
    /** Click handler */
    onClick?: React.MouseEventHandler<HTMLButtonElement>;
  };
  defaultComponent: D;
}

The props field contains your custom props merged with an optional generic P. The defaultComponent field specifies the default HTML element or component to render.

Step 2: Declare the OverridableComponent

Next, use the OverridableComponent type to declare your component. This creates the function overloads that handle both the default component case and the override case.

import { OverridableComponent } from '@mui/material/OverridableComponent';

declare const MyButton: OverridableComponent<MyButtonTypeMap>;

This declaration tells TypeScript that MyButton accepts either the default props or override props depending on whether the component prop is provided.

Step 3: Implement with ForwardRef

Finally, implement the component using React.forwardRef to ensure refs are properly forwarded. Use OverrideProps to type the props in the implementation.

import * as React from 'react';
import { OverrideProps } from '@mui/material/OverridableComponent';
import { Button as MuiButton } from '@mui/material';

const MyButton = React.forwardRef<HTMLButtonElement, OverrideProps<MyButtonTypeMap, 'button'>>(
  (props, ref) => {
    const { component: Component = 'button', myColor = 'primary', ...others } = props;
    
    return (
      <MuiButton
        component={Component as any}
        ref={ref}
        color={myColor}
        {...others}
      />
    );
  },
) as typeof MyButton;

export default MyButton;

The cast as typeof MyButton applies the full polymorphic type definition to the implementation, ensuring the component prop works correctly with various element types.

Using Your Typed Custom Component

Once implemented, your custom component provides full type safety when used with different root elements. The TypeScript compiler correctly infers available props based on the component prop value.

import React from 'react';
import MyButton from './MyButton';
import { Link } from '@mui/material';

export function Demo() {
  return (
    <>
      {/* Default button element - myColor and onClick available */}
      <MyButton myColor="secondary" onClick={() => alert('Clicked!')}>
        Default Button
      </MyButton>

      {/* Override to Link - href prop becomes available */}
      <MyButton component={Link} href="/home" myColor="primary">
        As Link
      </MyButton>

      {/* Override to div - standard HTML div props available */}
      <MyButton component="div" myColor="primary">
        Rendered as div
      </MyButton>
    </>
  );
}

When component={Link} is specified, TypeScript knows that href is a valid prop. When component="div" is used, standard HTML div attributes are accepted alongside your custom myColor prop.

Extending Existing Material-UI Components

You can also extend existing Material-UI components while preserving their polymorphic capabilities. This pattern is useful when you want to add custom props to built-in components like Button.

import * as React from 'react';
import { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent';
import { Button as MuiButton, ButtonTypeMap, ButtonProps } from '@mui/material/Button';
import { styled } from '@mui/material/styles';

// Extend the original TypeMap with custom props
type MyButtonProps = ButtonProps & { 
  myVariant?: 'ghost' | 'solid' 
};

type MyButtonTypeMap = ButtonTypeMap & { 
  props: MyButtonProps 
};

// Create the styled component
const StyledButton = styled(MuiButton, {
  shouldForwardProp: (prop) => prop !== 'myVariant',
})<{ myVariant?: 'ghost' | 'solid' }>(({ theme, myVariant }) => ({
  ...(myVariant === 'ghost' && {
    backgroundColor: 'transparent',
    border: `1px solid ${theme.palette.primary.main}`,
  }),
}));

// Declare the overridable component
declare const MyButton: OverridableComponent<MyButtonTypeMap>;

const MyButton = React.forwardRef<any, OverrideProps<MyButtonTypeMap, 'button'>>(
  (props, ref) => <StyledButton ref={ref} {...props} />,
) as typeof MyButton;

export default MyButton;

Because MyButtonTypeMap inherits from ButtonTypeMap, all default Button overloads stay intact, and the new myVariant prop is added to the component's own props. This approach leverages the same architecture found in packages/mui-material/src/Button/Button.d.ts.

Summary

  • Material-UI's polymorphic system relies on the OverridableComponent interface in packages/mui-material/src/OverridableComponent/index.ts to enable type-safe component overrides.
  • TypeMap structure requires defining both props (component-specific properties) and defaultComponent (default root element) to satisfy the OverridableTypeMap interface.
  • Implementation pattern involves declaring the component as OverridableComponent, then implementing using React.forwardRef with OverrideProps for the props parameter, and casting the result to preserve overloads.
  • Extending existing components works by intersecting the base component's TypeMap with your custom props, preserving all polymorphic capabilities while adding new functionality.

Frequently Asked Questions

What is the OverridableComponent interface in Material-UI?

The OverridableComponent interface is the core TypeScript utility that enables polymorphic behavior in Material-UI. Defined in packages/mui-material/src/OverridableComponent/index.ts, it uses function overloads to provide distinct type signatures for when you use the default root element versus when you override it with the component prop, ensuring full type safety across both scenarios.

How do I forward refs in typed custom Material-UI components?

Use React.forwardRef with the OverrideProps type from @mui/material/OverridableComponent. The generic parameters should specify the element type (e.g., HTMLButtonElement) and OverrideProps<YourTypeMap, 'defaultElement'>. After defining the forwardRef component, cast it as typeof YourComponent to apply the full OverridableComponent overloads.

Can I extend existing Material-UI components while keeping polymorphic types?

Yes, you can extend existing components by creating a new TypeMap that intersects with the base component's TypeMap, such as type MyTypeMap = ButtonTypeMap & { props: MyCustomProps }. This preserves all existing polymorphic capabilities from the base component while adding your custom props, allowing the component prop to work exactly as it does in the original Material-UI component.

Where are the TypeScript definitions for polymorphic components located?

The core TypeScript definitions for polymorphic components are located in packages/mui-material/src/OverridableComponent/index.ts within the mui/material-ui repository. Additional type utilities are available in packages/mui-types/src/index.ts. For reference implementations showing how built-in components use these types, examine packages/mui-material/src/Button/Button.d.ts and the test specifications in packages/mui-material/test/typescript/OverridableComponent.spec.tsx.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →