Home Strong typed Polymorphic component
Post
Cancel

Strong typed Polymorphic component

Creating an polymorphic component

In this example, we’ll leverage typescript to build a strong typed component that can have its only props instead of setting all props as optional props

  • strong typed component, let say if we have an Component called ‘MyComponent’ with as props to be img, then the compiler should give us a warning that img element need to have src and alt pros
1
<MyComponent as="img" src="" alt="" />

how can we achieve this?

  • we can set src and alt in the component props.like this:
1
2
3
4
5
6
7
8
9
10
11
12
type Props = {
  as: React.ElementType;
  children: React.ReactNode;
  src: string;
  alt: string;
};
const MyComponent = ({ as, children }: Props) => {
  const Component = as || "div";
  return <Component> {children}</Component>;
};

export default MyComponent;
  • the above method has a big issue,if the as props is set to be, let say, <p> element, the src and alt is not needed in the props. to this point, we can use generic to solve this issue.

  • and the component will looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Props<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
const MyComponent = <C extends React.ElementType>({
  as,
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "div";
  return <Component {...restProps}> {children}</Component>;
};

export default MyComponent;
  • in the above example, we have const Component = as || "div"; that means if the optional as is undefined, the Component will have a default value of div. but does Typescript know it? Look at the code below:
1
<MyComponent href="">hello world</MyComponent>
  • there would be no warning by Typescript which is no good, because MyComponent with no preset as props will become an div, and to set an href attribute to a div is not something good, we want TS to give us some waring.here is how we do it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Props<C extends React.ElementType> = {
  as?: C;
  children: React.ReactNode;
} & React.ComponentPropsWithoutRef<C>;
const MyComponent = <C extends React.ElementType = "div">({
  as,
  children,
  ...restProps
}: Props<C>) => {
  const Component = as || "div";
  return <Component {...restProps}> {children}</Component>;
};

export default MyComponent;
  • now we want to make the Props<C> ‘clean’ up a bit, so we can leverage React.PropsWithChildren and the code will look like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
  ElementType,
  ComponentPropsWithoutRef,
  PropsWithChildren,
} from "react";
type RainBow = "orange" | "yellow" | "purple" | "black" | "green" | "red";
type TextProps<C extends ElementType> = {
  as?: C;
  color?: RainBow | "lime";
};
type Props<C extends React.ElementType> = PropsWithChildren<TextProps<C>> &
  ComponentPropsWithoutRef<C>;

const MyComponent = <C extends ElementType = "div">({
  as,
  color = "lime",
  children,
  ...otherProps
}: Props<C>) => {
  const Component = as || "div";
  return <Component {...otherProps}>{children}</Component>;
};
export default MyComponent;
  • now it is time to make it reusable. we split as and color into two different props.
1
2
3
4
5
type AsProp<C extends React.ElementType> = {
  as?: C;
};

type TextProps = { color?: Rainbow | "black" };
  • and we can change the PolymorphicComponentProp utility definition to include the as prop, component props, and children prop:
1
2
3
4
5
6
7
8
type AsProp<C extends React.ElementType> = {
  as?: C;
};

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>>;
  • so the final result will looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type RainBow =
  | "red"
  | "orange"
  | "yellow"
  | "green"
  | "lime"
  | "blue"
  | "purple";

type TextProps = {
  color?: RainBow | "black";
};
type AsProp<C> = {
  as?: C;
};

type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);

type PolymorphicComponentProp<
  C extends React.ElementType,
  Props = {}
> = React.PropsWithChildren<Props & AsProp<C>> &
  Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;

export const Text = <C extends React.ElementType = "span">({
  as,
  children,
  color,
  ...restProps
}: PolymorphicComponentProp<C, TextProps>) => {
  const Component = as || "span";
  return <Component {...restProps}>{children}</Component>;
};
This post is licensed under CC BY 4.0 by the author.