参考文档:React TypeScript Cheatsheet
不使用React.FC
代码语言:javascript复制// Bad
const ViewDemo: React.FC<ViewDemoProps> = (props) => {
// ...
return (
<div>
这是使用React.FC类型声明的
</div>
)
}
// Good
const ViewDemo = (props: ViewDemoProps) => {
// ...
return (
<div>
这是不使用React.FC类型声明的
</div>
)
}
基本数据类型不需要显示声明
提供初始值后,boolean
、string
、number
类型可以通过类型推断得出
// Good
const loading = true
// Good
const CookieKey = 'cookie-key'
// Good
const maxCount = 10
useState
代码语言:javascript复制// Bad
const [state, setState] = useState<boolean>(false)
const [user, setUser] = useState<User>(null)
// Good
const [state, setState] = useState(false)
const [user, setUser] = useState<User | null>(null)
useRef
引用Dom
元素
// Bad
const divRef = useRef(null)
// etc...
<div ref={divRef}>etc</div>
// Good
const divRef = useRef<HTMLDivElement>(null);
// etc...
<div ref={divRef}>etc</div>
引用可变值,如定时器
代码语言:javascript复制// Bad
const intervalRef = useRef();
intervalRef.current = setInterval(() => {
console.log('setInterval')
}, 1000)
// Good
const intervalRef = useRef<number>();
useEffect(() => {
// 自己管理current的值
intervalRef.current = setInterval(() => {
console.log('setInterval')
}, 1000)
return () => clearInterval(intervalRef.current);
}, []);
createRef
代码语言:javascript复制// Bad
const divRef = createRef(null)
// etc...
<div ref={divRef}>etc</div>
// Good
const divRef = createRef<HTMLDivElement>(null);
// etc...
<div ref={divRef}>etc</div>
useReducer
Bad:没有声明state、action的类型
代码语言:javascript复制import React, { useReducer } from 'react'
const initialState = {count: 0}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count action.payload};
case 'decrement':
return {count: state.count - Number(action.payload)};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement', payload: '5'})}>-</button>
<button onClick={() => dispatch({type: 'increment', payload: 5})}> </button>
</>
);
}
Good:
代码语言:javascript复制import React, { useReducer } from 'react';
const initialState = {count: 0};
// 声明为可辨别联合类型
type ACTIONTYPE =
| { type: 'increment', payload: number}
| { type: 'decrement', payload: string}
// 使用typeof获取变量initialState的类型,如果数据较多,显示声明state类型
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return {count: state.count action.payload};
case 'decrement':
return {count: state.count - Number(action.payload)};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement', payload: '5'})}>-</button>
<button onClick={() => dispatch({type: 'increment', payload: 5})}> </button>
</>
);
}
Context
Bad:createContext
没有声明类型参数
import React, { createContext } from "react";
interface AppContextInterface {
name: string;
author: string;
url: string;
}
const AppCtx = createContext();
// Provider in your app
const sampleAppContext: AppContextInterface = {
name: "Using React Context in a Typescript App",
author: "thehappybug",
url: "http://www.example.com",
};
export const App = () => (
<AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);
// ---组件里使用---
import { useContext } from "react";
export const PostInfo = () => {
const appContext = useContext(AppCtx);
return (
<div>
Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
{appContext?.url}
</div>
);
};
Good
直接使用createContext
和useContext
代码语言:javascript复制import React, { createContext } from "react";
interface AppContextInterface {
name: string;
author: string;
url: string;
}
// 设置了类型AppContextInterface,但是没有提供默认值
const AppCtx = createContext<AppContextInterface | null>(null);
// Provider in your app
const sampleAppContext: AppContextInterface = {
name: "Using React Context in a Typescript App",
author: "thehappybug",
url: "http://www.example.com",
};
export const App = () => (
<AppCtx.Provider value={sampleAppContext}>...</AppCtx.Provider>
);
// ---组件里使用---
import { useContext } from "react";
export const PostInfo = () => {
const appContext = useContext(AppCtx);
return (
<div>
Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
{appContext?.url}
</div>
);
};
封装createCtx
函数
要类型不要默认值
代码语言:javascript复制// create-ctx.ts
import React, { createContext, useContext } from "react";
// context只指定类型,不提供默认值
export function createCtx<A extends {} | null>() {
// 将默认值设置为了undefined
const ctx = createContext<A | undefined>(undefined);
function useCtx() {
const c = useContext(ctx);
if (c === undefined)
throw new Error("useCtx must be inside a Provider with a value");
return c;
}
return [useCtx, ctx.Provider] as const; // 'as const' makes TypeScript infer a tuple
}
// ---------------使用Provider---------------------
// Provider.tsx
// import React from 'react'
// import { createCtx } from '*/create-ctx'
interface AppContextInterface {
name: string;
author: string;
url: string;
}
export const [useCtx, ContextProvider] = createCtx<AppContextInterface>()
const sampleAppContext: AppContextInterface = {
name: "Using React Context in a Typescript App",
author: "thehappybug",
url: "http://www.example.com",
};
export const App = () => (
<ContextProvider value={sampleAppContext}>...</ContextProvider>
);
// -----------------组件中使用-------------------
// import React from 'react'
// import { useCtx } from '*/Provide'
export const PostInfo = () => {
const appContext = useCtx();
return (
<div>
Name: {appContext?.name}, Author: {appContext?.author}, Url:{" "}
{appContext?.url}
</div>
);
};
在Typescript Playground中查看
要默认值不要类型
代码语言:javascript复制// create-ctx.ts
import React, { createContext, useContext, useState } from "react";
// context无指定类型,有默认值
export function createCtx<A>(defaultValue: A) {
type UpdateType = React.Dispatch<React.SetStateAction<typeof defaultValue>>;
const defaultUpdate: UpdateType = () => defaultValue;
// 自动添加了一个更新函数update
const ctx = createContext({
state: defaultValue,
update: defaultUpdate,
});
function Provider(props: React.PropsWithChildren<{}>) {
// update函数提供给外部,来更新state
const [state, update] = useState(defaultValue);
const val = useMemo(() => {
return {
state,
update
}
}, [state])
return <ctx.Provider value={val} {...props} />
}
function useCtx() {
const c = useContext(ctx);
if (c === undefined)
throw new Error("useCtx must be inside a Provider with a value");
return c;
}
return [useCtx, Provider] as const; // alternatively, [typeof ctx, typeof Provider]
}
// ---------------使用Provider---------------------
// Provider.tsx
// import React from 'react'
// import { createCtx } from '*/create-ctx'
const sampleAppContext = {
name: "Using React Context in a Typescript App",
author: "thehappybug",
url: "http://www.example.com",
};
// 虽然设置了类型AppContextInterface,但是没有提供默认值
export const [useCtx, ContextProvider] = createCtx(sampleAppContext)
export const App = () => (
<ContextProvider>...</ContextProvider> // <-- 这里不用再注入value
);
// ---------------组件中使用---------------------
// import React from 'react'
// import { useCtx } from '*/Provide'
export const PostInfo = () => {
const { state, update } = useCtx(); // <-- 多了update函数
return (
<div>
Name: {state?.name}, Author: {state?.author}, Url:{" "}
{state?.url}
</div>
);
};
在TypeScript Playground中查看
forwardRef
Bad:没有声明forwardRef泛型的类型参数
代码语言:javascript复制import React, { forwardRef, ReactNode, useRef } from "react"
export const FancyButton = forwardRef((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
// ----------使用------------
const App = () => {
const btnRef = useRef(null)
return (
<FancyButton type="button" ref={btnRef} />
)
}
Good
代码语言:javascript复制import React, { forwardRef, ReactNode, useRef } from "react";
interface Props {
children?: ReactNode;
type: "submit" | "button";
}
// 提供给使用FancyButton的地方使用
export type Ref = HTMLButtonElement;
export const FancyButton = forwardRef<Ref, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
// ----------使用------------
// import { Ref } from '*/FancyButton'
const App = () => {
const btnRef = useRef<Ref | null>(null)
return (
<FancyButton type="button" ref={btnRef} />
)
}
在TypeScript Playground中查看
如果不想要外部使用时再手动指定Ref
类型,可以修改为
import React, { forwardRef, ReactNode, Ref, useRef } from "react";
interface Props {
children?: ReactNode;
type: "submit" | "button";
}
export const FancyButton = forwardRef( // <-- 没有再指定泛型类型参数
(
props: Props,
ref: Ref<HTMLButtonElement> // <-- 限定参数类型
) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
)
);
// ----------------------
// 使用
// import { Ref } from '*/FancyButton'
const App = () => {
const btnRef = useRef(null) // <-- 这里不用再指定类型
return (
<FancyButton type="button" ref={btnRef} />
)
}
useImperativeHandle
Bad:没有声明useImperativeHandle
的类型
// Countdown.tsx
import React, { useImperativeHandle, forwardRef } from 'react'
const Countdown = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
start() {
alert("Start")
}
}))
return <div>Countdown</div>
})
Good:
代码语言:javascript复制// Countdown.tsx
import React, { useImperativeHandle, forwardRef } from 'react'
// 定义传给forwardRef的类型
export interface CountdownHandle {
start: () => void
}
// 组件本身的属性类型
interface CountdownProps {
time: number
}
const Countdown = forwardRef<CountdownHandle, CountdownProps>((props, ref) => {
useImperativeHandle(ref, () => ({
// start() has type inference here
start() {
alert("Start")
}
}))
return <div>Countdown</div>
})
使用Countdown
组件
import React, { useRef, useEffect } from 'react'
import Countdown, { CountdownHandle } from "./Countdown.tsx";
function App() {
const countdownEl = useRef<CountdownHandle>(null);
useEffect(() => {
if (countdownEl.current) {
// start() has type inference here as well
countdownEl.current.start();
}
}, []);
return <Countdown ref={countdownEl} />;
}
自定义Hook
Bad:实际返回的类型非期望的类型
代码语言:javascript复制import React from 'react'
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
return [isLoading, load];
}
Good:
代码语言:javascript复制import React from 'react'
export function useLoading() {
const [isLoading, setState] = React.useState(false);
const load = (aPromise: Promise<any>) => {
setState(true);
return aPromise.finally(() => setState(false));
};
// 使用as const将返回值限定为只读元组
return [isLoading, load] as const;
}
联合类型
使用联合类型时需要进行类型收窄
in 操作符收窄
in
操作符可以判断一个对象是否有对应的属性名,可以通过这个收窄对象类型
type LinkProps = Omit<JSX.IntrinsicElements["a"], "href"> & { to?: string };
function RouterLink(props: LinkProps | AnchorProps) {
// Good
if ("href" in props) {
return <a {...props} />;
} else {
return <Link {...props} />;
}
}
类型判断式(type predicates)
一个采用 parameterName is Type
的形式返回 boolean
值的函数,但 parameterName
必须是当前函数的参数名
// Button props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
href?: undefined;
};
// Anchor props
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href?: string;
};
// Input/output options
type Overload = {
(props: ButtonProps): JSX.Element;
(props: AnchorProps): JSX.Element;
};
// 通过in判断有无href,用is断言是否是AnchorProps类型
const hasHref = (props: ButtonProps | AnchorProps): props is AnchorProps =>
"href" in props;
// Component
const Button: Overload = (props: ButtonProps | AnchorProps) => {
// hasHref的返回值中用is断言了类型,所以这里可以推断出具体类型
if (hasHref(props)) return <a {...props} />;
// button render
return <button {...props} />;
};
在TypeScript Playground中查看
ButtonProps、AnchorProps也可以使用JSX.IntrinsicElements
声明
import React from 'react'
type ButtonProps = JSX.IntrinsicElements["button"];
type AnchorProps = JSX.IntrinsicElements["a"];
// Input/output options
type Overload = {
(props: ButtonProps): JSX.Element;
(props: AnchorProps): JSX.Element;
};
// 通过in判断有无href,用is断言是否是AnchorProps类型
const hasHref = (props: ButtonProps | AnchorProps): props is AnchorProps =>
"href" in props;
// Component
const Button: Overload = (props: ButtonProps | AnchorProps) => {
// hasHref的返回值中用is断言了类型,所以这里可以推断出具体类型
if (hasHref(props)) return <a {...props} />;
// button render
return <button {...props} />;
};
在TypeScript Playground中查看
可辨别联合(Discriminated unions)
Bad:
代码语言:javascript复制// Bad
interface Shape {
kind: 'circle' | 'square';
radius?: number;
sideLength?: number;
}
function getArea(shape: Shape) {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
if (shape.kind === 'square') {
return shape.sideLength ** 2
// Object is possibly 'undefined'
}
return 0
}
Good:
代码语言:javascript复制interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
if (shape.kind === 'square') {
return shape.sideLength ** 2
}
return 0
}
穷尽检查(Exhaustiveness checking)
利用任何类型都不能赋值给 never
类型(除了 never
自身)的特性,实现穷尽检查。
我们可以将上例中最后的return 0
进行优化
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
if (shape.kind === 'square') {
return shape.sideLength * shape.sideLength
}
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
这样,当我们给Shape
增加其他类型时,就会有ts
报错,提醒我们必须处理新加的类型
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
if (shape.kind === 'square') {
return shape.sideLength * shape.sideLength
}
// error
const _exhaustiveCheck: never = shape;
// Type 'Rectangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
Typeof
JavaScript 本身就提供了 typeof
操作符,可以返回运行时一个值的基本类型信息:
- "string"
- "number"
- "bigInt"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
可以看出,typeof可以用来收窄基础类型string
、number
、boolean
、symbol
function padLeft(padding: number | string, input: string) {
// ok
console.log(padding.valueOf())
// Good
if (typeof padding === "number") {
return new Array(padding 1).join(" ") input;
}
return padding input;
}
但不能收窄null或具体的对象类型
代码语言:javascript复制function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
枚举
可以用联合类型代替的
代码语言:javascript复制// Bad
export enum Postion {
left = 'left',
right = 'right',
top = 'top',
bottom = 'bottom'
}
// Good
export type Position = "left" | "right" | "top" | "bottom";
类组件
Bad:未指定组件props
、state
的类型
import React from 'react'
class App extends React.Component {
state = {
count: 0
}
render() {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
Good:
代码语言:javascript复制import React from 'react'
// 声明class组件的props类型,也可以使用 `interface`
interface MyProps {
message: string;
};
// 声明class组件的state类型
interface MyState {
count: number;
};
// 指定App的props类型为MyProps,state类型为MyState
class App extends React.Component<MyProps, MyState> {
// 这里再次声明state类型为MyState,是为了方便初始化(有代码提示)
state: MyState = {
count: 0
}
render() {
return (
<div>
{this.props.message} {this.state.count}
</div>
);
}
}
事件
内联形式
Good:内联形式event
的类型会通过类型推断得到
import React from 'react'
const el = (
<button
onClick={(event) => {
console.log('event的类型会通过类型推断得到')
}}
/>
);
事件函数:
Bad:没有声明e
的类型,handleChange
没有使用useCallback
包裹
import React from 'react'
const App = () => {
const handleChange = (e) => {
console.log('value=', e.currentTarget.value)
}
return (
<div>
<input type="text" handleChange={handleChange} />
</div>
)
}
Good:
使用FormEvent<T>
代码语言:javascript复制import React, { useCallback } from 'react'
const App = () => {
// 根据 = 号右边的函数类型推断出handleChange的类型
const handleChange = useCallback((e: React.FormEvent<HTMLInputElement>): void => {
console.log('value=', e.currentTarget.value)
}, [])
return (
<div>
<input type="text" handleChange={handleChange} />
</div>
)
}
使用React.ChangeEventHandler<T>
代码语言:javascript复制import React, { useCallback } from 'react'
const App = () => {
// 类型声明在 = 号左侧
const handleChange: React.ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
console.log('value=', e.currentTarget.value)
}, [])
return (
<div>
<input type="text" handleChange={handleChange} />
</div>
)
}
不关心事件类型:React.SyntheticEvent
代码语言:javascript复制import React, { useCallback, useRef } from 'react'
const Form = () => {
const formRef = useRef<HTMLFormElement>(null)
const handleSubmit = useCallback((e: React.SyntheticEvent) => {
e.preventDefault();
// typeof获取e.target的类型
// & 通过交叉给e.target类型扩展自定义的字段
// as 将e.target断言为指定类型
// 这样,e.target就可以访问email、password属性
const target = e.target as typeof e.target & {
email: { value: string };
password: { value: string };
};
const email = target.email.value; // typechecks!
const password = target.password.value; // typechecks!
// etc...
}, [])
return (
<form
ref={formRef}
onSubmit={handleSubmit}
>
<div>
<label>
Email:
<input type="email" name="email" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Log in" />
</div>
</form>
)
}
参考:事件类型对照表
回调函数中的可选参数
回调函数中不应该使用可选参数。也就是说,调用callback时,要提供所有所需参数,是否使用这些参数应该由使用者自己决定
代码语言:javascript复制// Bad
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i ) {
callback(arr[i], i);
}
}
// Good
function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
for (let i = 0; i < arr.length; i ) {
callback(arr[i], i);
}
}
获取组件的Props:React.ComponentType
封装工具类:$ElementProps<T>
// react-utility-types.d.ts
import React from 'react'
export type $ElementProps<T> = T extends React.ComponentType<infer Props>
? Props extends object
? Props
: never
: never;
使用
代码语言:javascript复制import React from "react";
import { Modal } from 'antd'
import { $ElementProps } from "*/react-utility-types";
const CustomModal = (
{ title, children, ...props }: $ElementProps<typeof Modal> // new utility, see below
) => (
<div {...props}>
{title}: {children}
</div>
);