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 havesrc
andalt
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 ofdiv
. 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, becauseMyComponent
with no presetas
props will become andiv
, and to set anhref
attribute to adiv
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 leverageReact.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
andcolor
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>;
};