React的ts类型检查与注意点
配置
首先进行编译配置。配置很简单,先安装@types/react包(npm i -D @types/react),然后在tsconfig.json的编译配置项compilerOptions中配置"jsx"项为"react"即可。
*jsx项的选项
"preserve":模式下对代码的编译会保留 jsx 格式,并输出一份 .jsx 后缀的文件;
"react":模式下对代码的编译是直接转换成 React.createElement,并输出一份 .js 后缀的文件;
"react-native":模式下对代码的编译会保留 jsx 格式,并输出一份 .js 后缀的文件。
| Mode |
Input |
Output |
Output File Extension |
| preserve |
<div /> |
<div /> |
.jsx |
| react |
<div /> |
React.createElement("div") |
.js |
| react-native |
<div /> |
<div /> |
.js |
常用检查
函数组件的类型检查
使用FC(Function Component)类型来声明函数组件
FC定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)。如
1 2 3 4 5 6 7 8 9 10
| import React, { FC } from 'react';
interface DemoComponentProps { className?: string; style?: React.CSSProperties; }
const DemoComponent: FC<DemoComponentProps> = props => { return <div>hello react</div>; };
|
也可以直接使用普通函数来进行组件声明,这种形式更加灵活:
1 2 3 4 5 6 7 8 9 10
| interface DemoComponentProps { className?: string; style?: React.CSSProperties; children?: React.ReactNode; }
function DemoComponent(props: DemoComponentProps) { return <div>hello react</div>; }
|
与之类似的还有SFC,但现在已经不再建议使用了,具体原因可见下文@types/react源码分析。
定义 Props 类型
通常使用 ComponentName|Props 的格式命名 Props 类型。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React from 'react'
interface DemoComponentProps { title: string; content: string; }
function DemoComponent (props: DemoComponentProps) { const { title, content, } = props; return (<> <p>title: {title}</p> <p>content: {content}</p> </>) }
|
Children
我们通过设置 React.ReactNode 来设置children。如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React from 'react'
type CardProps = { title: string; children: React.ReactNode; };
export function Card({ title, children }: CardProps) { return ( <section className="cards"> <h2>{title}</h2> {children} </section> ); }
|
默认 props 声明
TypeScript 3.0 开始已经支持了 defaultProps ,这也意味着我们可以很方便的定义默认值。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import React, { PropsWithChildren } from 'react';
export interface HelloProps { name: string; }
const Hello = ({ name }: PropsWithChildren<HelloProps>) => <div>Hello {name}!</div>;
Hello.defaultProps = { name: 'Wayne' };
|
这种方式很简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型:
1
| Hello.defaultProps = { name: 'Wayne' } as Partial<HelloProps>;
|
Dispatch接口
Dispatch<any> 泛型接口,用于定义dispatch的类型,常用于useReducer生成的dispatch中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
const asyncAction = (dispatch: Dispatch<any>) => { return { asyncAddaction() { console.log('执行addActions之前: ' + Date.now()); setTimeout(() => { console.log('执行addActions : ' + Date.now()); dispatch(addActions()); }, 1000); } } }
|
泛型函数组件
泛型函数组件在列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React from 'react';
export interface ListProps<T> { list: T[]; renderItem: (item: T, index: number) => React.ReactNode; }
export function List<T>(props: ListProps<T>) { return ( <section> { props.list.map(props.renderItem) } </section> ); }
function TestList() { return ( <List list={[1, 2, 3]} renderItem={i => { /* TypeScript推断i为number类型 */ return (<p>{i}</p>) }} /> ); }
|
子组件声明
使用Parent.Child形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制,可以避免命名冲突,ant design中就大量使用了这类形式。相比ParentChild这种命名方式, Parent.Child更为优雅些。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React, { PropsWithChildren } from 'react';
export interface LayoutProps {} export interface LayoutHeaderProps {} export interface LayoutFooterProps {}
export function Layout(props: PropsWithChildren<LayoutProps>) { return <div className="layout">{props.children}</div>; }
// 作为父组件的属性 Layout.Header = (props: PropsWithChildren<LayoutHeaderProps>) => { return <div className="header">{props.children}</div>; };
Layout.Footer = (props: PropsWithChildren<LayoutFooterProps>) => { return <div className="footer">{props.children}</div>; };
function TestLayout () { return (<Layout> <Layout.Header>header</Layout.Header> <Layout.Footer>footer</Layout.Footer> </Layout>) }
|
Forwarding Refs
React.forwardRef 在 16.3 新增, 可以用于转发 ref,适用于 HOC 和函数组件。其暴露方法可以使用 ComponentName|Methods的命名规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React, { useState, useImperativeHandle, FC, useRef, useCallback } from 'react';
export interface MyModalProps { title?: React.ReactNode; onOk?: () => void; onCancel?: () => void; }
export interface MyModalMethods { show(): void; }
export const MyModal = React.forwardRef<MyModalMethods, MyModalProps>((props, ref) => { const [visible, setVisible] = useState();
useImperativeHandle(ref, () => ({ show: () => setVisible(true), }));
return <Modal visible={visible}>...</Modal>; });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const Test: FC<{}> = props => { const modal = useRef<MyModalMethods | null>(null); const confirm = useCallback(() => { if (modal.current) { modal.current.show(); } }, []);
const handleOk = useCallback(() => {}, []);
return ( <div> <button onClick={confirm}>show</button> <MyModal ref={modal} onOk={handleOk} /> </div> ); };
|
类组件的类型检查
继承 Component<P, S={}> 或 PureComponent<P, S={}> 泛型类,接收两个参数:
P:props的类型定义
S:state的类型定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| import React from 'react'
export interface CounterProps { defaultCount: number; }
interface State { count: number; }
export class Counter extends React.Component<CounterProps, State> {
public static defaultProps = { defaultCount: 0, };
public state = { count: this.props.defaultCount, };
public componentDidMount() {} public componentWillUnmount() {}
public componentDidCatch() {}
public componentDidUpdate(prevProps: CounterProps, prevState: State) {}
public render() { return ( <div> {this.state.count} <button onClick={this.increment}>Increment</button> <button onClick={this.decrement}>Decrement</button> </div> ); }
private increment = () => { this.setState(({ count }) => ({ count: count + 1 })); };
private decrement = () => { this.setState(({ count }) => ({ count: count - 1 })); }; }
|
使用static defaultProps定义默认 props
在 defaultProps 中定义的 props 可以不需要?可选操作符修饰,demo如上一个。
子组件声明
类组件可以使用静态属性形式声明子组件,如:
1 2 3 4 5 6 7 8 9 10 11 12
| import React from 'react' import Header from './Header' import Footer from './Footer'
export class Layout extends React.Component<LayoutProps> { public static Header = Header; public static Footer = Footer;
public render() { return <div className="layout">{this.props.children}</div>; } }
|
泛型组件
与函数组件的泛型组件类似,如:
1 2 3 4 5
| import React from 'react'
export class List<T> extends React.Component<ListProps<T>> { public render() {} }
|
其他函数组件的类型定义
HTML元素的扩展属性
你可以通过 JSX.IntrinsicElements 集合确保你能够设置一个元素的所有HTML属性。如:
1 2 3 4 5 6 7
| import React from 'react'
type ButtonProps = JSX.IntrinsicElements["button"];
function Button({ ...allProps }: ButtonProps) { return <button {...allProps} />; }
|
预设属性:
1 2 3 4 5 6 7 8 9 10
| import React from 'react'
type ButtonProps = Omit<JSX.IntrinsicElements["button"], "type">;
function Button({ ...allProps }: ButtonProps) { return <button type="button" {...allProps} />; }
const z = <Button type="button">Hi</Button>;
|
Context
Context 提供了一种跨组件间状态共享机制。通常我们使用 Name|ContextValue 的命名规范声明Context的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import React, { FC, useContext } from 'react';
export interface Theme { primary: string; secondary: string; }
export interface ThemeContextValue { theme: Theme; onThemeChange: (theme: Theme) => void; }
export const ThemeContext = React.createContext<ThemeContextValue>({ theme: { primary: 'red', secondary: 'blue', }, onThemeChange: noop, });
export const ThemeProvider: FC<{ theme: Theme; onThemeChange: (theme: Theme) => void }> = props => { return ( <ThemeContext.Provider value={{ theme: props.theme, onThemeChange: props.onThemeChange }}> {props.children} </ThemeContext.Provider> ); };
/** * 暴露hooks, 以use{Name}命名 */ export function useTheme() { return useContext(ThemeContext); }
|
*高阶组件的类型检查
老实说不建议使用HOC。高阶组件笨重且难以理解,容易造成嵌套地狱(wrapper),对 Typescript 类型化也不友好。不过还是举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import React, { FC } from 'react';
export interface ThemeProps { primary: string; secondary: string; }
export function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
interface OwnProps {}
type WithThemeProps = P & OwnProps;
const WithTheme = (props: WithThemeProps) => { const fakeTheme: ThemeProps = { primary: 'red', secondary: 'blue', }; return <Component {...fakeTheme} {...props} />; };
WithTheme.displayName = `withTheme${Component.displayName}`;
return WithTheme; }
const Foo: FC<{ a: number } & ThemeProps> = props => <div style={{ color: props.primary }} />; const FooWithTheme = withTheme(Foo); () => { <FooWithTheme a={1} />; };
|
或
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
type HOC<InjectedProps, OwnProps = {}> = <P>( Component: React.ComponentType<P & InjectedProps>, ) => React.ComponentType<P & OwnProps>;
export interface ThemeProps { primary: string; secondary: string; }
export const withTheme: HOC<ThemeProps> = Component => props => { const fakeTheme: ThemeProps = { primary: 'red', secondary: 'blue', }; return <Component {...fakeTheme} {...props} />; };
|
*Render Props
React 的 props(包括 children)并没有限定类型,它可以是一个函数。于是就有了 render props, 这是和高阶组件一样常见的模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import React from 'react';
export interface ThemeConsumerProps { children: (theme: Theme) => React.ReactNode; }
export const ThemeConsumer = (props: ThemeConsumerProps) => { const fakeTheme = { primary: 'red', secondary: 'blue' }; return props.children(fakeTheme); };
<ThemeConsumer> {({ primary }) => { return <div style={{ color: primary }} />; }} </ThemeConsumer>;
|
事件的类型定义
使用handleEvent命名事件处理器.
如果存在多个相同事件处理器, 则按照 handle|Type|Event 的格式命名, 例如: handleNameChange。
1 2 3 4 5 6 7 8 9 10
| import React from 'react';
export const EventDemo: FC<{}> = props => { const handleClick = useCallback<React.MouseEventHandler>(evt => { evt.preventDefault(); }, []);
return <button onClick={handleClick} />; };
|
事件Events
常用 Event 事件对象类型:
ClipboardEvent<T = Element> 剪贴板事件对象
DragEvent<T = Element> 拖拽事件对象
ChangeEvent<T = Element> Change 事件对象
KeyboardEvent<T = Element> 键盘事件对象
MouseEvent<T = Element> 鼠标事件对象
TouchEvent<T = Element> 触摸事件对象
WheelEvent<T = Element> 滚轮事件对象
AnimationEvent<T = Element> 动画事件对象
TransitionEvent<T = Element> 过渡事件对象
FormEvent:一个react的form表单event的类型
demos:
1 2 3 4
| <form onSubmit={(e:FormEvent)=>{ e.preventDefault(); }}>
|
1 2 3 4 5 6
| <input type="text" value={count} onChange={(e: ChangeEvent<HTMLInputElement>) => { setCount(e.currentTarget.value); }} />
|
内置事件处理器的类型
@types/react内置了以下事件处理器的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }['bivarianceHack']; type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>; type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>; type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>; type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>; type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>; type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>; type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>; type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>; type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>; type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>; type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>; type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>; type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>; type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>; type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
|
可以简洁地声明事件处理器类型:
1 2 3 4 5 6 7 8 9 10 11
| import { ChangeEventHandler } from 'react'; export const EventDemo: FC<{}> = props => {
const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(evt => { console.log(evt.target.value); }, []);
return <input onChange={handleChange} />; };
|
自定义组件暴露事件处理器类型
和原生 html 元素一样, 自定义组件应该暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型
自定义事件处理器类型以 ComponentName|Event|Handler 的格式命名。 为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { FC, useState } from 'react';
export interface UploadValue { url: string; name: string; size: number; }
export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;
export interface UploadProps { value?: UploadValue; onChange?: UploadChangeHandler; }
export const Upload: FC<UploadProps> = props => { return <div>...</div>; };
|
其他类型检查
获取原生元素 props 定义
有些场景我们希望原生元素扩展一下一些 props. 所有原生元素 props 都继承了React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如InputHTMLAttributes. 具体可以参考React.createElement方法的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import React, { FC } from 'react';
export function fixClass< T extends Element = HTMLDivElement, Attribute extends React.HTMLAttributes<T> = React.HTMLAttributes<T> >(cls: string, type: keyof React.ReactHTML = 'div') { const FixedClassName: FC<Attribute> = props => { return React.createElement(type, { ...props, className: `${cls} ${props.className}` }); };
return FixedClassName; }
const Container = fixClass('card'); const Header = fixClass('card__header', 'header'); const Body = fixClass('card__body', 'main'); const Footer = fixClass('card__body', 'footer');
const Test = () => { return ( <Container> <Header>header</Header> <Body>header</Body> <Footer>footer</Footer> </Container> ); };
|
styled-components
styled-components 是流行的CSS-in-js库, Typescript 在 2.9 支持泛型标签模板,因此可以简单地对 styled-components 创建的组件进行类型约束
1 2 3 4 5 6 7 8 9 10 11
| import styled from 'styled-components/macro';
const Title = styled.h1<{ active?: boolean }>` color: ${props => (props.active ? 'red' : 'gray')}; `;
const NewHeader = styled(Header)<{ customColor: string }>` color: ${props => props.customColor}; `;
|
为没有提供 Typescript 声明文件的第三方库自定义模块声明
1 2 3 4 5 6 7 8
|
declare module 'awesome-react-component' { import * as React from 'react'; export const Foo: React.FC<{ a: number; b: string }>; }
|
axios的类型定义
1 2 3 4 5 6 7 8 9 10 11
| import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse,AxiosError } from 'axios' const server: AxiosInstance = axios.create(); server.interceptors.request.use((config: AxiosRequestConfig) => { return config; }); server.interceptors.response.use((res: AxiosResponse) => { if (res.status === 200) { res = res.data; } return res; },(err:AxiosError)=>{});
|
Preact的类型检查
首先tsconfig.json配置与React不同:
1 2 3 4 5 6 7 8 9
| { "compilerOptions": { "jsx": "preserve", "jsxFactory": "h", // Preact的虚拟DOM解析 "jsxFragmentFactory": "Fragment" // Preact的fragment组件解析 ... } ... }
|
并且比较“坑”的是,需要在每个组件中引入Fragment和h,用以告知ts的解析模式。
1
| import { Fragment, h } from 'preact'
|
@types/react源码分析
@types/react的源码其实就是仔细定义了一些React涉及到的接口,源码的备注也比较完整。源码地址:github @types/react
SFC(Stateless Function Component)和FC
早前一直好奇SFC和FC的区别,在看源码时发现
1 2 3 4 5 6 7 8 9 10
| type SFC<P = {}> = FunctionComponent<P>; type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> { (props: PropsWithChildren<P>, context?: any): ReactElement | null; propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; }
|
在目前的@types/react定义中,SFC和FC指向都是FunctionComponent这个接口,也就是说它们是一样的。
主要原因可见DefinitelyTyped pull-30364,简单来说,SFC是被弃用的,但为保证兼容旧业务和提升语义性仍然保留。总的结果就是React.SFC、React.StatelessComponent、React.FC、React.FunctionComponent都是同一个接口
*以前的SFC
1 2 3 4 5 6 7 8
| type SFC<P = {}> = StatelessComponent<P>; interface StatelessComponent<P = {}> { (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null; propTypes?: ValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; }
|
发现区别其实就是propTypes项ValidationMap和WeakValidationMap的区别(type PropsWithChildren<P> = P & { children?: ReactNode };,ValidationMap的定义可见@types/prop-types),即旧SFC的props校验更为严格:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export type ValidationMap<T> = { [K in keyof T]?: Validator<T[K]> };
type WeakValidationMap<T> = { [K in keyof T]?: null extends T[K] ? Validator<T[K] | null | undefined> : undefined extends T[K] ? Validator<T[K] | null | undefined> : Validator<T[K]> };
export interface Validator<T> { (props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null; [nominalTypeHack]?: T; }
|
Events
@types/react花了很大的篇幅进行了事件接口的封装,毕竟DOM是前端中最复杂的模块之一了。如触摸事件的封装:
1 2 3 4 5 6 7 8 9 10 11 12 13
| interface TouchEvent<T = Element> extends SyntheticEvent<T, NativeTouchEvent> { altKey: boolean; changedTouches: TouchList; ctrlKey: boolean;
getModifierState(key: string): boolean; metaKey: boolean; shiftKey: boolean; targetTouches: TouchList; touches: TouchList; }
|
@types/react中的Content接口
1 2 3 4 5
| interface Context<T> { Provider: Provider<T>; Consumer: Consumer<T>; displayName?: string; }
|
可以发现我们需要传递一个类型,从而使得里面的参数类型也是一致。
其他
@types/react有许多设计比较巧妙的地方,可以通过《@types react 值得注意的 TS 技巧》进行引读。
注意点
1.不要直接使用export default导出匿名函数组件
如
1 2 3
| export default (props: {}) => { return <div>hello react</div>; };
|
这种方式导出的组件在React Inspector查看时会显示为Unknown。可修改为:
1 2 3
| export default Hello (props: {}) => { return <div>hello react</div>; };
|
2.放弃PropTypes
有了 Typescript 之后可以安全地约束 Props 和 State, 没有必要引入 React.PropTypes, 而且它的表达能力比较弱
3.关于是否使用FC的争议
关于是否使用FC一直存在争议,如《typescript-react-why-i-dont-use-react-fc》,其中给了5条理由,总得来说就是不用FC会更加灵活和更具拓展性。
以我个人的观点来看,FC会让代码更具语义性,如果能保证项目没有迁移类React技术栈(preact、taro、rax等)的情况下,建议使用。
相关链接
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com