用TypeScript写React的时候,我们可以通过@types/...
使用一些类型检查,本文记录这些检查方式的注意点。
首先进行编译配置。配置很简单,先安装@types/react
包(npm i -D @types/react
),然后在tsconfig.json
的编译配置项compilerOptions
中配置"jsx"
项为"react"
即可。
"preserve"
:模式下对代码的编译会保留 jsx 格式,并输出一份 .jsx
后缀的文件;"react"
:模式下对代码的编译是直接转换成 React.createElement,并输出一份 .js
后缀的文件;"react-native"
:模式下对代码的编译会保留 jsx 格式,并输出一份 .js
后缀的文件。Mode | Input | Output | Output File Extension |
---|---|---|---|
preserve |
|
|
.jsx |
react |
|
React.createElement("div") |
.js |
react-native |
|
|
.js |
FC定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)。如
import React, {
FC } from 'react';
interface DemoComponentProps {
className?: string;
style?: React.CSSProperties;
}
const DemoComponent: FC<DemoComponentProps> = props => {
return <div>hello react</div>;
};
也可以直接使用普通函数来进行组件声明,这种形式更加灵活:
interface DemoComponentProps {
className?: string;
style?: React.CSSProperties;
// 手动声明children
children?: React.ReactNode;
}
function DemoComponent(props: DemoComponentProps) {
return <div>hello react</div>;
}
与之类似的还有SFC,但现在已经不再建议使用了,具体原因可见下文
@types/react
源码分析。
通常使用 ComponentName|Props
的格式命名 Props 类型。如:
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>
</>)
}
我们通过设置 React.ReactNode 来设置children。如
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>
);
}
TypeScript 3.0 开始已经支持了 defaultProps ,这也意味着我们可以很方便的定义默认值。如:
import React, {
PropsWithChildren } from 'react';
export interface HelloProps {
name: string;
}
/**
* 直接使用函数参数声明
* PropsWithChildren只是扩展了children, 完全可以自己声明
* type PropsWithChildren = P & {
* children?: ReactNode;
* }
*/
const Hello = ({
name }: PropsWithChildren<HelloProps>) => <div>Hello {
name}!</div>;
Hello.defaultProps = {
name: 'Wayne' };
这种方式很简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型:
Hello.defaultProps = {
name: 'Wayne' } as Partial<HelloProps>;
Dispatch
泛型接口,用于定义dispatch的类型,常用于useReducer生成的dispatch中。
/**
* 创建一个异步action的函数,返回一个包含异步action对象
*/
const asyncAction = (dispatch: Dispatch<any>) => {
return {
asyncAddaction() {
// 一个异步的添加action
console.log('执行addActions之前: ' + Date.now());
setTimeout(() => {
console.log('执行addActions : ' + Date.now());
dispatch(addActions());
}, 1000);
}
}
}
泛型函数组件在列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:
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更为优雅些。如:
import React, {
PropsWithChildren } from 'react';
export interface LayoutProps {
}
export interface LayoutHeaderProps {
} // 采用ParentChildProps形式命名
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>)
}
React.forwardRef
在 16.3 新增, 可以用于转发 ref,适用于 HOC 和函数组件。其暴露方法可以使用 ComponentName|Methods
的命名规则
/* MyModal.tsx */
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();
// 初始化ref暴露的方法
useImperativeHandle(ref, () => ({
show: () => setVisible(true),
}));
return <Modal visible={
visible}>...</Modal>;
});
/* Test.tsx */
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
或 PureComponent
泛型类,接收两个参数:P
:props的类型定义S
:state的类型定义import React from 'react'
export interface CounterProps {
// 同样是{ComponentName}Props形式命名
defaultCount: number; // 可选props, 不需要?修饰
}
/**
* 组件状态
*/
interface State {
count: number;
}
/**
* 继承React.Component, 并声明Props和State类型
*/
export class Counter extends React.Component<CounterProps, State> {
/**
* 默认参数
*/
public static defaultProps = {
defaultCount: 0,
};
/**
* 初始化State
*/
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 }));
};
}
在 defaultProps 中定义的 props 可以不需要?
可选操作符修饰,demo如上一个。
类组件可以使用静态属性形式声明子组件,如:
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>;
}
}
与函数组件的泛型组件类似,如:
import React from 'react'
export class List<T> extends React.Component<ListProps<T>> {
public render() {
}
}
你可以通过 JSX.IntrinsicElements
集合确保你能够设置一个元素的所有HTML属性。如:
import React from 'react'
type ButtonProps = JSX.IntrinsicElements["button"];
function Button({
...allProps }: ButtonProps) {
return <button {
...allProps} />;
}
预设属性:
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 提供了一种跨组件间状态共享机制。通常我们使用 Name|ContextValue
的命名规范声明Context的类型。
import React, {
FC, useContext } from 'react';
export interface Theme {
primary: string;
secondary: string;
}
export interface ThemeContextValue {
theme: Theme;
onThemeChange: (theme: Theme) => void;
}
/**
* 创建Context, 并设置默认值, 以 Name|Context 的格式命名
*/
export const ThemeContext = React.createContext<ThemeContextValue>({
theme: {
primary: 'red',
secondary: 'blue',
},
onThemeChange: noop,
});
/**
* Provider, 以{Name}Provider命名
*/
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 类型化也不友好。不过还是举个栗子:
import React, {
FC } from 'react';
export interface ThemeProps {
primary: string;
secondary: string;
}
/**
* 给指定组件注入'主题'
*/
export function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
/**
* WithTheme 自己暴露的Props
*/
interface OwnProps {
}
/**
* 高阶组件的props, 忽略ThemeProps, 外部不需要传递这些属性
*/
type WithThemeProps = P & OwnProps;
/**
* 高阶组件
*/
const WithTheme = (props: WithThemeProps) => {
// 假设theme从context中获取
const fakeTheme: ThemeProps = {
primary: 'red',
secondary: 'blue',
};
return <Component {
...fakeTheme} {
...props} />;
};
WithTheme.displayName = `withTheme${
Component.displayName}`;
return WithTheme;
}
// Test
const Foo: FC<{
a: number } & ThemeProps> = props => <div style={
{
color: props.primary }} />;
const FooWithTheme = withTheme(Foo);
() => {
<FooWithTheme a={
1} />;
};
或
/**
* 抽取出通用的高阶组件类型
*/
type HOC<InjectedProps, OwnProps = {
}> = <P>(
Component: React.ComponentType<P & InjectedProps>,
) => React.ComponentType<P & OwnProps>;
/**
* 声明注入的Props
*/
export interface ThemeProps {
primary: string;
secondary: string;
}
export const withTheme: HOC<ThemeProps> = Component => props => {
// 假设theme从context中获取
const fakeTheme: ThemeProps = {
primary: 'red',
secondary: 'blue',
};
return <Component {
...fakeTheme} {
...props} />;
};
React 的 props(包括 children)并没有限定类型,它可以是一个函数。于是就有了 render props, 这是和高阶组件一样常见的模式:
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);
};
// Test
<ThemeConsumer>
{
({
primary }) => {
return <div style={
{
color: primary }} />;
}}
</ThemeConsumer>;
如果存在多个相同事件处理器, 则按照 handle|Type|Event
的格式命名, 例如: handleNameChange
。
import React from 'react';
export const EventDemo: FC<{
}> = props => {
const handleClick = useCallback<React.MouseEventHandler>(evt => {
evt.preventDefault();
// ...
}, []);
return <button onClick={
handleClick} />;
};
常用 Event 事件对象类型:
ClipboardEvent
剪贴板事件对象
DragEvent
拖拽事件对象
ChangeEvent
Change 事件对象
KeyboardEvent
键盘事件对象
MouseEvent
鼠标事件对象
TouchEvent
触摸事件对象
WheelEvent
滚轮事件对象
AnimationEvent
动画事件对象
TransitionEvent
过渡事件对象
FormEvent
:一个react的form表单event的类型
demos:
<form
onSubmit={
(e:FormEvent)=>{
e.preventDefault();//取消默认事件
}}>
<input
type="text"
value={
count}
onChange={
(e: ChangeEvent<HTMLInputElement>) => {
setCount(e.currentTarget.value);// HTMLInputElement表示这个一个html的input节点
}} />
@types/react
内置了以下事件处理器的类型:
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>>;
可以简洁地声明事件处理器类型:
import {
ChangeEventHandler } from 'react';
export const EventDemo: FC<{
}> = props => {
/**
* 可以限定具体Target的类型
*/
const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(evt => {
console.log(evt.target.value);
}, []);
return <input onChange={
handleChange} />;
};
和原生 html 元素一样, 自定义组件应该暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型
自定义事件处理器类型以 ComponentName|Event|Handler
的格式命名。 为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀
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 都继承了React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如InputHTMLAttributes. 具体可以参考React.createElement方法的实现
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;
}
/**
* Test
*/
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 是流行的CSS-in-js库, Typescript 在 2.9 支持泛型标签模板,因此可以简单地对 styled-components 创建的组件进行类型约束
// 依赖于@types/styled-components
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};
`;
// global.d.ts
// 自定义模块声明
declare module 'awesome-react-component' {
// 依赖其他模块的声明文件
import * as React from 'react';
export const Foo: React.FC<{
a: number; b: string }>;
}
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)=>{
});
首先tsconfig.json配置与React不同:
{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "h", // Preact的虚拟DOM解析
"jsxFragmentFactory": "Fragment" // Preact的fragment组件解析
...
}
...
}
并且比较“坑”的是,需要在每个组件中引入Fragment和h,用以告知ts的解析模式。
import {
Fragment, h } from 'preact'
@types/react
源码分析@types/react
的源码其实就是仔细定义了一些React涉及到的接口,源码的备注也比较完整。源码地址:github @types/react
早前一直好奇SFC和FC的区别,在看源码时发现
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都是同一个接口
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项 = P & { children?: ReactNode };ValidationMap
和WeakValidationMap
的区别(type PropsWithChildren
,ValidationMap的定义可见@types/prop-types
),即旧SFC的props校验更为严格:
// ValidationMap
export type ValidationMap<T> = {
[K in keyof T]?: Validator<T[K]> };
// WeakValidationMap
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]>
};
// Validator
export interface Validator<T> {
(props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
[nominalTypeHack]?: T;
}
@types/react
花了很大的篇幅进行了事件接口的封装,毕竟DOM是前端中最复杂的模块之一了。如触摸事件的封装:
interface TouchEvent<T = Element> extends SyntheticEvent<T, NativeTouchEvent> {
altKey: boolean;
changedTouches: TouchList;
ctrlKey: boolean;
/**
* See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
*/
getModifierState(key: string): boolean;
metaKey: boolean;
shiftKey: boolean;
targetTouches: TouchList;
touches: TouchList;
}
@types/react
中的Content接口 interface Context<T> {
Provider: Provider<T>;
Consumer: Consumer<T>;
displayName?: string;
}
可以发现我们需要传递一个类型,从而使得里面的参数类型也是一致。
@types/react
有许多设计比较巧妙的地方,可以通过《@types react 值得注意的 TS 技巧》进行引读。
如
export default (props: {
}) => {
return <div>hello react</div>;
};
这种方式导出的组件在React Inspector查看时会显示为Unknown。可修改为:
export default Hello (props: {
}) => {
return <div>hello react</div>;
};
有了 Typescript 之后可以安全地约束 Props 和 State, 没有必要引入 React.PropTypes, 而且它的表达能力比较弱
关于是否使用FC一直存在争议,如《typescript-react-why-i-dont-use-react-fc》,其中给了5条理由,总得来说就是不用FC会更加灵活和更具拓展性。
以我个人的观点来看,FC会让代码更具语义性,如果能保证项目没有迁移类React技术栈(preact、taro、rax等)的情况下,建议使用。