優雅的在 React 中使用 TS

學習常見 React Prop Types,例如跟使用者互動的 Event Types、children 該用什麼 type、客製元件該怎麼使用 ComponentProps 繼承 native element 等等

Hannah Lin
Hannah Lin
14 min readNov 1, 2023

--

TypeScript 系列文

1. The Very Basics for TS
2. Generics 的使用情境
3. The `extends` keyword
4
. GenericsMapped 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.ElementReact.ReactElement 就是拿來訂義 React components children 的型別,他們之間的差異如下

  • React.ReactNode:accepts everything React can render
  • React.ReactElement | JSX.Element:only accepts JSX

在 React 世界,JSX.ElementReact.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

ReactNode type 包含 JSX 跟其他原始型別
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 本來就有 valuetype這些屬性啊!這時用 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>;
直接可以使用第三方套件的 props type

取得 ref 的屬性型別
Get the Props of an Element with the Associated Ref

另外網路上也會看到類似語法 React.ComponentPropsWithoutRefReact.ComponentPropsWithRef,功能其實是一樣的主要只差在 ref ,可以參考之前寫過的文章 什麼是 ref?

Event Types

捷徑: 使用 React.ComponentProps<T>[props]讓 TS 自動填入 Event 型別,就不用去死記每一個 Event Types 了

在寫一些跟使用者有互動的地方都會用到 Event,例如 onClickonMouseMoveonChange 等等,不同的 Event 都會對應到不同的 Event Types,本來想逐一介紹,但實在是太多了根本記不起來

List of 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

--

--