優雅的在 React 中使用 TS
學習常見 React Prop Types,例如跟使用者互動的 Event Types、children 該用什麼 type、客製元件該怎麼使用 ComponentProps 繼承 native element 等等
TypeScript 系列文
1. The Very Basics for TS
2. Generics 的使用情境
3. The `extends` keyword
4. Generics — Mapped Types
5. 用生活例子圖解 Utility Types
6. 優雅的在 React 中使用 TS
7. 用 ts-migrate 仙女棒讓 JS 專案瞬間 migrate 成 TS
🔖 文章索引
1. Children as Props
2. How to type (extend) HTML elements
3. Event Types
4. CSS Styles as props
這一篇會針對 React 專屬的 types 來說明,也是自己覺得有點難入門的部分
export declare interface AppProps {
children?: React.ReactNode; // best, accepts everything React can render
childrenElement: JSX.Element; // A single React element
style?: React.CSSProperties; // to pass through style props
onChange?: React.FormEventHandler<HTMLInputElement>; // form events! the generic parameter is the type of event.target
// more info: https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase/#wrappingmirroring
props: Props & React.ComponentProps<"button">; // to impersonate all the props of a button element and explicitly not forwarding its ref
props2: Props & React.ComponentProps<MyButtonWithForwardRef>; // to impersonate all the props of MyButtonForwardedRef and explicitly forwarding its ref
}
Children as Props
建議只使用
React.ReactNode
作為children
的型別因為他可以接受 React render 的所有東西
只要寫過 React 一定知道 children
是寫元件常會出現的
function MyComponent (props) {
return <>{props.children}</>
}
而 React.ReactNode
,JSX.Element
跟 React.ReactElement
就是拿來訂義 React components children 的型別,他們之間的差異如下
React.ReactNode:
accepts everything React can renderReact.ReactElement | JSX.Element:
only accepts JSX
在 React 世界,JSX.Element
跟 React.ReactElement
幾乎一模一樣,都是代表原生 DOM 或客製 component 的型別
const node: JSX.Element = <div /> || <MyComponent />;
const node2: React.ReactElement = <div /> || <MyComponent />;
但他們卻無法代表所有 React 可以 render 的型別像 string, number, null…
所以建議使用 React.ReactNode
為 component children 的型別才符合所有不同 case
const Component = ({ children }: {
children: React.ReactNode;
}) => {
return <div>{children}</div>;
};
// 可以 pass JSX, string, number 到這個元件裡
<Component>hello world</Component>
<Component>{123}</Component>
<Component>{undefined}</Component>
<Component>
<div>Hello world</div>
</Component>
How to type (extend) HTML elements
使用
React.ComponentProps<T>
讓客製化元件繼承原生元件的屬性型別 (props type),就不用一個一個重覆寫
在 React 中客製元件是很常見的事,也會列出此元件的所有屬性型別 (eg. value: string
),但若是客製 button
, input
, text
這種 default 就存在很多屬性的
開發者不可能全部重定義一次,這時候就可以用 ComponentProps
繼承 native HTML element 所有屬性的型別
舉例來說,當你新增了一個客製元件 <Button>
,就需要去定義它會有的所有屬性型別,例如 value
是字串、type
可能是 button | submit | text| undefined
等…
type ButtonProps = {
value?: string
type?: 'button' | 'submit' | 'text' | undefined
taste: string
}
const Button = ({ value, type }: ButtonProps) => {
return <button type={type} value={value}></button>
}
但原生 button
本來就有 value
、type
這些屬性啊!這時用 React.ComponentProps<”button”>
來繼承原生 <Button>
的所有屬性型別,就不用花大把時間重覆定義屬性型別了
// type 或 interface 都可以
type ButtonProps = React.ComponentProps<"button"> & {
taste: string;
};
interface ButtonProps extends React.ComponentProps<"button"> {
taste?: string;
}
function MyButton({...props }: ButtonProps) {
return <button {...props} />;
}
若只想取得元件的單一屬性型別而不是全部的話可以用 React.ComponentProps<T>[props]
React.ComponentProps<"input">["onChange"]
React.ComponentProps<"button">["type"]
覆蓋原生元件屬性型別 Overriding Native Props
可以使用
Omit<ComponentProps<T>, props> & {props : xxx}
去覆蓋原生屬性型別
當客製一個按鈕,除了沿用所有按鈕屬性還要修改一部分已有的屬性時,也可以使用 ComponentProps
// 按鈕的 type 要改成 'circle' | 'square' | 'rectangle'
type ButtonProps = Omit<ComponentProps<"button", 'type'> & {
type: 'circle' | 'square' | 'rectangle';
};
取得已經存在的客製元件屬性型別
Get the Props of a Component
想要繼承已經寫好的元件屬性型別,也可以使用
React.ComponentProps<typeof T>
const BananaBtn = (props: { taste }) => {
return <button taste={props.taste}>Submit</button>;
};
type MyBananaBtnProps = ComponentProps<typeof BananaBtn> & {
number?: string;
}
const MyBananaBtn = ({ ...props }: MyButtonProps) {
return <button number={props.number} {...props} />;
}
這可以讓你專心去擴充元件而不用擔心型別問題,甚至使用第三方的元件也一樣,有些第三方套件無法 export 他的 type ,但還是可以用 ComponentProps<typeof T>
去抓到那些型別。
import { ComponentProps } from "react";
import { Button } from "some-external-library";
type ButtonProps = ComponentProps<typeof Button>;
取得 ref 的屬性型別
Get the Props of an Element with the Associated Ref
另外網路上也會看到類似語法 React.ComponentPropsWithoutRef
跟 React.ComponentPropsWithRef
,功能其實是一樣的主要只差在 ref
,可以參考之前寫過的文章 什麼是 ref?
Event Types
捷徑: 使用
React.ComponentProps<T>[props]
讓 TS 自動填入 Event 型別,就不用去死記每一個 Event Types 了
在寫一些跟使用者有互動的地方都會用到 Event,例如 onClick
、onMouseMove
、onChange
等等,不同的 Event 都會對應到不同的 Event Types,本來想逐一介紹,但實在是太多了根本記不起來
後來看到 有人介紹一種懶人方法 (延伸閱讀 : Event Types in React and TypeScript),運用React.ComponentProps
可以讓 TS 自動填入 Event 的型別,真的是太神奇了!
不使用 React.ComponentProps
必須去記各種不同 Events 名稱
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }
<input onChange={handleChange} ... />
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
}
<input onChange={onChange} ... />
const handleMouseMove = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleMouseMove} ... />
要背超冗長的 Events 名稱 React.ReactEventHandler<HTMLInputElement>
、React.ChangeEvent<HTMLInputElement>
、React.MouseEvent<HTMLDivElement>
,當然也可以 hover 偷看 TS 給的提示,但還是需要複製貼上落落長的 Type很麻煩
使用 React.ComponentProps
無腦使用
const handleChange: React.ComponentProps<"input">["onChange"] = (ev) => { ... }
<input onChange={handleChange} ... />
const handleMouseMove: React.ComponentProps<"div">["onMouseMove"] = (ev) => { ... }
<div onMouseMove={handleMouseMove} ... />
完全不用記,讓 TS 做剩下的事,真的是太方便了
這時 hover,不管是 ReactEventHandler
還是 Event
TS 都幫忙自動填入
CSS Styles as props
用
React.CSSProperties
取得 style props
style props 相對單純很多,只要跟樣式有關的型別找他準沒錯
interface IHelloProps {
message: string;
style?: React.CSSProperties;
}
export const HelloWorld = ({ message, style }: IHelloProps) => (
<h1 style={style}>Hello {message ?? "World"}!</h1>
);
<HelloWorld message="Hannah" style={{width: '30px'}}/>
若只想取得單一樣式屬性型別就用 React.CSSProperties[props]
paddingRight?: React.CSSProperties['paddingRight']
popoverWidth?: React.CSSProperties['width']
其他
其實此文只列出一些常見的 props type,其他像 React.FC
雖然在許多 codebase 還是看到他的身影經,但已經不推薦使用了。
關於 React Hooks 的 typing 本來想單獨寫一篇文,但其實他比想像中容易很多,所以我會陸續加在 Hooks 系列文的最後面,對於我自己查找也比較容易
竟然長達半年沒更新文了,
近期實在發生太多事,除了顧嫩嬰最重大的就是我從美國搬回台灣啦,
希望也能更新一下為什麼會回來 (有人會看嗎?沒關係我自己想紀錄 XD)
Reference
- React with TypeScript Cheatsheet : React 官方推薦的 up to date
- React.ReactNode vs JSX.Element vs React.ReactElement