警告:我做了一个翻译 pr, 还没有 merge,所以我先开一个分支来看了。
WARNING:I did a translation pr, but not merged to origin repo yet, so pre-build a branch to read ahead.
"这个指南是一个最新的摘要,记录了关于如何用 TypeScript 以函数式风格使用 React(以及相关生态)最重要的模式和示例。它会使你的代码在从具体实现中进行类型推导时绝对是类型安全的,这样就能减少来自过度类型声明的信息噪音,并更容易写出易于长期维护的正确类型声明。"
觉得有帮助?想要更多更新?
🎉 现在更新支持到 TypeScript v3.7 🎉
🚀 _升级到 [email protected]
🚀
- 完全的类型安全(支持
--strict
模式),并且在向应用的下游代码传递时,不会丢失类型信息(比如:缺少类型断言或用any
来强行使用) - 使用高级 TypeScript 语言特性(诸如类型推论和控制流分析)来消除类型冗余、使类型声明简明扼要
- 使用专门的 TypeScript 补充库 来减少类型代码的重复性和复杂度
- typesafe-actions - 为 Redux / Flux 架构中 "action-creators" 创造的类型安全实用工具集
- utility-types - TypeScript 常用泛型集合,能够补充 TS 自带的映射类型和别名 - 把它当成类型复用的 lodash。
- react-redux-typescript-scripts - 开发者工具配置文件,可用于遵循本指南的项目
- Todo-App playground: Codesandbox
- React, Redux, TypeScript - RealWorld App: Github | Demo
查看位于 /playground
文件夹中的 Playground 项目。它包含本指南所有的代码示例的源文件。它们都已使用最新版本的 TypeScript 和第三方类型定义包(诸如 @types/react
和 @types/react-redux
)进行了测试,以确保示例是最新的,且没有随着类型定义升级而失效(基于 create-react-app --typescript
)。
我们创建了该 Playground 项目以便你可以简单地克隆到本地,并立即尝试本指南中所有的组件模式。它可以使你无需自己创建复杂的环境配置,直接在真实的项目环境中学习本指南的所有示例。
你能贡献并帮助改进本项目,如果你计划做出贡献,请查看我们的贡献指南:CONTRIBUTING.md
你也能通过赞助 issues 提供帮助。 通过 IssueHunt 平台进行赞助,bug 修复或功能请求之类的 issues 可以更快得到解决。
我强烈建议你赞助自己期待解决的 issue,以便增加它的优先级并吸引贡献者解决。
🌟 - 新内容及更新板块
- 用 TypeScript 写 React & Redux - 完全指南
- 安装
- React - 类型定义速查表
React.FC<Props>
|React.FunctionComponent<Props>
React.Component<Props, State>
React.ComponentType<Props>
React.ComponentProps<typeof XXX>
React.ReactElement
|JSX.Element
React.ReactNode
React.CSSProperties
React.HTMLProps<HTMLXXXElement>
React.ReactEventHandler<HTMLXXXElement>
React.XXXEvent<HTMLXXXElement>
- React - 类型模式
- Redux - 类型模式
- 配置和开发者工具
- 食谱
npm i -D @types/react @types/react-dom @types/react-redux
"react" - @types/react
"react-dom" - @types/react-dom
"redux" - (types included with npm package)*
"react-redux" - @types/react-redux
*提示: 本指南的类型系统适用于 Redux >= v4.x.x。如果想用于 Redux v3.x.x 请查看 这个配置)
表示函数组件的类型
const MyComponent: React.FC<Props> = ...
表示 class 组件的类型
class MyComponent extends React.Component<Props, State> { ...
表示 (React.FC | React.Component) 集合的类型 - 用于 HOC
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...
取得�组件 XXX 的 Props 类型(警告:无法用于静态声明的 default props 以及泛型 props)
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
表示 React 中 Element 概念的类型 - 表示一个原生 DOM 组件(比如 <div />
)或用户自定义的复合组件 (比如 <MyComponent />
)
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
表示任意类型的 React 节点(相当于 ReactElement (包括 Fragments 和 Portals) + 原始 JS 类型)
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;
const Component = ({ children: React.ReactNode }) => ...
表示 JSX 中样式对象的类型 - 实现 css-in-js 风格
const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...
表示指定 HTML 元素的类型 - 用于扩展 HTML 元素
const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }
<Input about={...} accept={...} alt={...} ... />
表示 event handler 的泛型类型 - 用于声明 event handlers
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }
<input onChange={handleChange} ... />
表示更多特殊 event。一些常见的 event 例如:ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent
。
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} ... />
上一段代码中的 React.MouseEvent<HTMLDivElement>
表示鼠标事件的类型,这个事件挂载在 HTMLDivElement
上。
::codeblock='playground/src/components/fc-counter.tsx'::
- 组件的 属性展开
::codeblock='playground/src/components/fc-spread-attributes.tsx'::
::codeblock='playground/src/components/class-counter.tsx'::
::codeblock='playground/src/components/class-counter-with-default-props.tsx'::
- 易于生成不同类型的变种组件,同时复用公共逻辑
- 常见的用例是泛型列表组件
::codeblock='playground/src/components/generic-list.tsx'::
将 children 用作 render prop 的简单组件
::codeblock='playground/src/components/name-provider.tsx'::
Mouse
组件的例子来源于 Render Props - React 文档
::codeblock='playground/src/components/mouse-provider.tsx'::
https://zh-hans.reactjs.org/docs/higher-order-components.html
给无状态的计数器加上状态
::codeblock='playground/src/hoc/with-state.tsx':: ::expander='playground/src/hoc/with-state.usage.tsx'::
用 componentDidCatch 给任意组件加上错误处理功能
::codeblock='playground/src/hoc/with-error-boundary.tsx':: ::expander='playground/src/hoc/with-error-boundary.usage.tsx'::
用 componentDidCatch 给任意组件加上错误处理功能
::codeblock='playground/src/hoc/with-connected-count.tsx':: ::expander='playground/src/hoc/with-connected-count.usage.tsx'::
::codeblock='playground/src/connected/fc-counter-connected.tsx':: ::expander='playground/src/connected/fc-counter-connected.usage.tsx'::
::codeblock='playground/src/connected/fc-counter-connected-own-props.tsx':: ::expander='playground/src/connected/fc-counter-connected-own-props.usage.tsx'::
::codeblock='playground/src/connected/fc-counter-connected-bind-action-creators.tsx':: ::expander='playground/src/connected/fc-counter-connected-bind-action-creators.usage.tsx'::
::codeblock='playground/src/context/theme-context.ts'::
::codeblock='playground/src/context/theme-provider.tsx'::
::codeblock='playground/src/context/theme-consumer.tsx'::
::codeblock='playground/src/context/theme-consumer-class.tsx'::
https://zh-hans.reactjs.org/docs/hooks-reference.html#usestate
::codeblock='playground/src/hooks/use-state.tsx'::
用于函数组件的状态管理 Hook (类似 Redux)。
::codeblock='playground/src/hooks/use-reducer.tsx'::
https://zh-hans.reactjs.org/docs/hooks-reference.html#usecontext
::codeblock='playground/src/hooks/use-theme-context.tsx'::
可以作为 import,使用 Redux connect
方法连接组件时,能够确保类型安全性
可以作为 import,用于不同层次中(reducers, sagas 或 redux-observables epics)接收和发送 redux actions
::codeblock='playground/src/store/types.d.ts'::
当创建 store 实例时,我们不需要编写任何额外的类型,它会通过类型推断自动建立一个类型安全的 Store 实例。
生成的 store 实例中的方法(像
getState
和dispatch
)将支持类型检查,并能够暴露所有的类型错误。
::codeblock='playground/src/store/index.ts'::
我们将使用成熟的辅助库
typesafe-actions
它被设计成便于使用 TypeScript 来写 Redux。
查看这个进阶教程来学习更多:Typesafe-Actions - Tutorial!
下面的方案用一个简单的工厂函数来自动创建类型安全的 action creators。目的是减少重复的 actions 和 creators 类型声明代码,并减少代码维护工作。生成结果是绝对类型安全的 action-creators 及其 actions。
::codeblock='playground/src/features/counters/actions.ts':: ::expander='playground/src/features/counters/actions.usage.ts'::
用 readonly
修饰符声明 reducer 中 State
的类型,可以获得编译时的不可变性
export type State = {
readonly counter: number;
readonly todos: ReadonlyArray<string>;
};
Readonly 修饰符允许初始化,但不允许重新赋值(编译器会提示错误)
export const initialState: State = {
counter: 0,
}; // OK
initialState.counter = 3; // TS Error: cannot be mutated
这对 JS 中的 数组 很起效,因为用 (push
, pop
, splice
, ...) 这样的赋值方法将会报错,但是 (concat
, map
, slice
,...) 这样的不可变方法依然是允许的。
state.todos.push('Learn about tagged union types') // TS Error: Property 'push' does not exist on type 'ReadonlyArray<string>'
const newTodos = state.todos.concat('Learn about tagged union types') // OK
这意味着 readonly
修饰符在对象的嵌套结构中不会向下传递不变性。你需要标记每个层级的每个属性。(译注:Readonly
是浅比较的)
小贴士: 使用
Readonly
或ReadonlyArray
映射类型
export type State = Readonly<{
counterPairs: ReadonlyArray<Readonly<{
immutableCounter1: number,
immutableCounter2: number,
}>>,
}>;
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated
为了解决上述问题,我们可以使用 DeepReadonly
类型(来自 utility-types
)。
import { DeepReadonly } from 'utility-types';
export type State = DeepReadonly<{
containerObject: {
innerValue: number,
numbers: number[],
}
}>;
state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated
state.containerObject.innerValue = 1; // TS Error: cannot be mutated
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods
为了理解下一小节,请确保了解 类型推论,基于控制流的类型分析 以及 标记联合类型
::codeblock='playground/src/features/todos/reducer.ts'::
请注意,我们不需要在 API 上使用任何泛型类型参数。可以和传统的 reducer 写法进行比较,它们是等价的。
::codeblock='playground/src/features/todos/reducer-ta.ts'::
::codeblock='playground/src/features/todos/reducer.spec.ts'::
::codeblock='playground/src/features/todos/epics.ts'::
::codeblock='playground/src/features/todos/epics.spec.ts'::
::codeblock='playground/src/features/todos/selectors.ts'::
注意:在下面一段代码中,只有关于 connect 类型声明背后概念的简短说明。请查看 Redux 连接组件 章节了解更多更具体的例子
import MyTypes from 'MyTypes';
import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux';
import { connect } from 'react-redux';
import { countersActions } from '../features/counters';
import { FCCounter } from '../components';
// Type annotation for "state" argument is mandatory to check
// the correct shape of state object and injected props you can also
// extend connected component Props interface by annotating `ownProps` argument
const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({
count: state.counters.reduxCounter,
});
// "dispatch" argument needs an annotation to check the correct shape
// of an action object when using dispatch function
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
bindActionCreators({
onIncrement: countersActions.increment,
}, dispatch);
// shorter alternative is to use an object instead of mapDispatchToProps function
const dispatchToProps = {
onIncrement: countersActions.increment,
};
// Notice we don't need to pass any generic type parameters to neither
// the connect function below nor map functions declared above
// because type inference will infer types from arguments annotations automatically
// This is much cleaner and idiomatic approach
export const FCCounterConnected =
connect(mapStateToProps, mapDispatchToProps)(FCCounter);
// You can add extra layer of validation of your action creators
// by using bindActionCreators generic type parameter and RootAction type
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
bindActionCreators<ActionCreatorsMapObject<Types.RootAction>>({
invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... }
}, dispatch);
注意:使用 thunk action creators 时你需要使用 bindActionCreators
。只有这样,你才能获得正确的 dispatch props 类型签名,如下所示。
警告: 目前(2019 四月)最新版 redux-thunk
中的 bindActionCreators
签名不会像下面那样正常工作,你需要使用 /playground/typings/redux-thunk/index.d.ts
中改良的类型定义并覆写 tsconfig 中的 paths
字段,像这样: "paths":{"redux-thunk":["typings/redux-thunk"]}
。
const thunkAsyncAction = () => async (dispatch: Dispatch): Promise<void> => {
// dispatch actions, return Promise, etc.
}
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
bindActionCreators(
{
thunkAsyncAction,
},
dispatch
);
type DispatchProps = ReturnType<typeof mapDispatchToProps>;
// { thunkAsyncAction: () => Promise<void>; }
/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */
// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }
通用的、跨项目的、 TS 相关的 npm scripts
"prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)",
"prettier:fix": "prettier --write 'src/**/*.ts'",
"lint": "tslint -p ./",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u"
"ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test",
我们有推荐的 tsconfig.json
配置文件,你可以借助 react-redux-typescript-scripts
方便地把它添加到你的项目里。
::expander='playground/tsconfig.json'::
https://www.npmjs.com/package/tslib
这个库通过把运行时辅助函数外置化,而不是内嵌到每个文件中,来减少你的打包文件大小。
安装
npm i tslib
把这行加到你的 tsconfig.json
中:
"compilerOptions": {
"importHelpers": true
}
https://palantir.github.io/tslint/
安装
npm i -D tslint
如果用于 React 项目,你应该加上额外的
react
规则集:npm i -D tslint-react
https://github.com/palantir/tslint-react
我们有推荐配置文件,你可以借助 react-redux-typescript-scripts
方便地把它添加到你的项目里。
::expander='playground/tslint.json'::
https://eslint.org/
https://typescript-eslint.io
安装
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
我们有推荐配置文件,他会自动添加 TypeScript 的解析器和插件,你可以借助 react-redux-typescript-scripts
方便地把它添加到你的项目里。
::expander='playground/.eslintrc'::
安装
npm i -D jest ts-jest @types/jest
::expander='configs/jest.config.json'::
::expander='configs/jest.stubs.js'::
不。用了 TypeScript 之后,没有必要再使用 PropTypes。当声明 Props 和 State 接口后,你将通过静态类型检查获得完全的自动补全和编码时的安全性。这样,你就能直接避免运行时错误,并减少大量调试时间。额外的好处是,这也是一种用于在源码中解释组件公共 API 的优雅而标准化的方法。
从实际来看,使用 interface
声明在编译错误时会生成一个 interface 同名标识,相反 type
别名不会生成标识名,并且会展开显示所有属性和嵌套的类型。
尽管我大部分时候更喜欢用 type
,但是有时候编译错误过于冗长影响排查,我会根据两者的差别,改用 interface 来隐藏报错中没那么重要的类型细节。
相关的 ts-lint
规则:https://palantir.github.io/tslint/rules/interface-over-type-literal/
一个常见的适应性方案是使用文件夹模块模式,这样你可以根据情况同时使用具名和默认 import。 这个方案的好处是你能实现更好的封装,以及能够安全地重构内部命名和文件夹结构,而不影响你的业务代码:
// 1. create your component files (`select.tsx`) using default export in some folder:
// components/select.tsx
const Select: React.FC<Props> = (props) => {
...
export default Select;
// 2. in this folder create an `index.ts` file that will re-export components with named exports:
// components/index.ts
export { default as Select } from './select';
...
// 3. now you can import your components in both ways, with named export (better encapsulation) or using default export (internal access):
// containers/container.tsx
import { Select } from '@src/components';
or
import Select from '@src/components/select';
...
首选新语法来进行 class 属性初始化
class ClassCounterWithInitialCount extends React.Component<Props, State> {
// default props using Property Initializers
static defaultProps: DefaultProps = {
className: 'default-class',
initialCount: 0,
};
// initial state using Property Initializers
state: State = {
count: this.props.initialCount,
};
...
}
首选新语法,用箭头函数声明 class 方法字段
class ClassCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
...
}
(译注:环境声明(ambient) 和 模块扩展(augmentation))
若要进行 module 扩展,import 应该位于 module 声明外部。
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';
declare module 'rxjs/Subject' {
interface Subject<T> {
lift<R>(operator: Operator<T, R>): Observable<R>;
}
}
创建第三方类型定义时,所有 imports 应该位于 module 声明内部,否则 imports 将被视为扩展并报错。
declare module "react-custom-scrollbars" {
import * as React from "react";
export interface positionValues {
...
如果你找不到第三方模块的类型声明,你可以自己写一个,或借助 Shorthand Ambient Modules 禁用该模块的类型检查。
::codeblock='playground/typings/modules.d.ts'::
如果你想为(自带类型定义的)某些 npm 模块使用替代的(自定义的)类型定义,你可以通过覆写编译选项中 paths
字段来实现。
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"redux": ["typings/redux"], // use an alternative type-definitions instead of the included one
...
},
...,
}
}
外部类型定义文件(*.d.ts)相关问题的处理策略
// added missing autoFocus Prop on Input component in "[email protected]" npm package
declare module '../node_modules/antd/lib/input/Input' {
export interface InputProps {
autoFocus?: boolean;
}
}
// fixed broken public type-definitions in "[email protected]" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';
declare module 'rxjs/Subject' {
interface Subject<T> {
lift<R>(operator: Operator<T, R>): Observable<R>;
}
}
更多搭配第三方类型定义的进阶场景可以在 TypeScript 官方文档 找到
相关进阶教程精选清单
高阶组件:
感谢这些优秀的人 (emoji key):
这个项目遵循 all-contributors 规范。欢迎任意形式的贡献!
MIT License
Copyright (c) 2017 Piotr Witek [email protected] (http://piotrwitek.github.io)