Skip to content

Instantly share code, notes, and snippets.

@pjchender
Last active June 27, 2021 16:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pjchender/1470fdd34c5aa2acdfa6036a919fd61e to your computer and use it in GitHub Desktop.
Save pjchender/1470fdd34c5aa2acdfa6036a919fd61e to your computer and use it in GitHub Desktop.
Polymorphic Components with TypeScript
// https://frontendmasters.com/courses/react-typescript/polymorphic-components/
// STEP 1:增加 as 這個 props 的型別定義
// as 的型別是泛型 E、它需要滿足 React.ElementType、且預設值為 React.ElementType
type ButtonBaseProps<E extends React.ElementType = React.ElementType> = {
children: string;
as?: E;
};
type PrimaryButtonProps = {
primary: boolean;
secondary?: never;
destructive?: never;
};
type SecondaryButtonProps = {
primary?: never;
secondary: boolean;
destructive?: never;
};
type DestructiveButtonProps = {
primary?: never;
secondary?: never;
destructive: boolean;
};
// STEP 2:定義 <Button /> 實際接收的 Props 型別
// 除了 ButtonProps 中定義的 as 和 children 之外,須包含 as 傳入的 E 的 props 的型別(React.ComponentProps<E>)
// 但需要把 ButtonBaseProps 中原本的 props(as, children)排除,也就是 Omit<React.ComponentProps<E>, keyof ButtonBaseProps>
// 因為 ButtonBaseProps 中包含 "as",但 React.ComponentProps<E> 沒有 as
// 如果沒有排除 keyof ButtonBaseProps 的話,E 會被 TS 推導成是 any
// 因為 "button" 這個 element 預設是沒有 as 這個型別
type ButtonProps<E extends React.ElementType> = ButtonBaseProps<E> &
(PrimaryButtonProps | SecondaryButtonProps | DestructiveButtonProps) &
Omit<React.ComponentProps<E>, keyof ButtonBaseProps>;
const createClassNames = (classes: { [key: string]: boolean }): string => {
let classNames = '';
for (const [key, value] of Object.entries(classes)) {
if (value) classNames += `${key} `;
}
return classNames.trim();
};
// STEP 3:
// Button<E> 的 E 會根據回傳的值(ButtonProps<E>)來自動推導(type argument inference)
// E 的預設值會是 typeof defaultElement 也就是 "button"
const defaultElement = 'button';
function Button<E extends React.ElementType = typeof defaultElement>({
children,
primary = false,
secondary = false,
destructive = false,
as,
...props
}: ButtonProps<E>) {
const classNames = createClassNames({ primary, secondary, destructive });
// STEP 4:動態回傳不同 tag 的 HTML element
const TagName = as || defaultElement;
return (
<TagName className={classNames} {...props}>
{children}
</TagName>
);
}
const Application = () => {
return (
<main>
{/* STEP 5:使用 href 但沒有告知 as="a" 時,TS 會報錯 */}
<Button primary as="a" href="example.com">
Primary
</Button>
<Button secondary>Secondary</Button>
<Button destructive>Destructive</Button>
</main>
);
};
export default Application;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment