Last active
June 27, 2021 16:02
-
-
Save pjchender/1470fdd34c5aa2acdfa6036a919fd61e to your computer and use it in GitHub Desktop.
Polymorphic Components with TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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