"這個指南是一個 最新的摘要,記錄了關於如何用 TypeScript 以函數式風格使用 React(以及相關生態)最重要的模式和示例。它會使你的代碼在 從具體實現中進行類型推導時絕對是 類型安全的,這樣就能減小來自過分類型聲明的信息噪音,並更容易寫出易於長期維護的正確類型聲明。"
--strict
模式),而且在嚮應用的下游代碼傳遞時,不會丟失類型信息(好比:缺乏類型斷言或用 any
來強行使用)🌟 - 新內容及更新板塊javascript
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 - 類型模式html
Class Componentsnode
泛型組件react
Render Propsgit
高階組件github
Redux 鏈接組件web
Contexttypescript
- 配置和開發者工具
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 請查看 這個配置)
React.FC<Props>
| React.FunctionComponent<Props>
表示函數組件的類型
const MyComponent: React.FC<Props> = ...
React.Component<Props, State>
表示class組件的類型
class MyComponent extends React.Component<Props, State> { ...}
React.ComponentType<Props>
表示 (React.FC | React.Component) 集合的類型 - 用於 HOC
const withState = <P extends WrappedComponentProps>( WrappedComponent: React.ComponentType<P>, ) => { ...}
React.ComponentProps<typeof XXX>
取得組件 XXX 的 Props 類型(警告:沒法用於靜態聲明的 default props 以及泛型 props)
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
React.ReactElement
| JSX.Element
表示 React 中 Element 概念的類型 - 表示一個原生 DOM 組件(好比 <div />
)或用戶自定義的複合組件 (好比 <MyComponent />
)
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
React.ReactNode
表示任意類型的 React 節點(至關於 ReactElement (包括 Fragments 和 Portals) + 原始 JS 類型)
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />; const Component = ({ children: React.ReactNode }) => ...
React.CSSProperties
表示 JSX 中樣式對象的類型 - 實現 css-in-js 風格
const styles: React.CSSProperties = { flexDirection: 'row', ... const element = <div style={styles} ...
React.HTMLProps<HTMLXXXElement>
表示指定 HTML 元素的類型 - 用於擴展 HTML 元素
const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... } <Input about={...} accept={...} alt={...} ... />
React.ReactEventHandler<HTMLXXXElement>
表示 event handler 的泛型類型 - 用於聲明 event handlers
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } <input onChange={handleChange} ... />
React.XXXEvent<HTMLXXXElement>
表示更多特殊 event。一些常見的 event 例如:ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent
。
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... } <div onMouseMove={handleChange} ... />
上一段代碼中的 React.MouseEvent<HTMLDivElement>
表示鼠標事件的類型,這個事件掛載在 HTMLDivElement
上。
import * as React from 'react'; type Props = { label: string; count: number; onIncrement: () => void; }; export const FCCounter: React.FC<Props> = props => { const { label, count, onIncrement } = props; const handleIncrement = () => { onIncrement(); }; return ( <div> <span> {label}: {count} </span> <button type="button" onClick={handleIncrement}> {`Increment`} </button> </div> ); };
import * as React from 'react'; type Props = { className?: string; style?: React.CSSProperties; }; export const FCSpreadAttributes: React.FC<Props> = props => { const { children, ...restProps } = props; return <div {...restProps}>{children}</div>; };
import * as React from 'react'; type Props = { label: string; }; type State = { count: number; }; export class ClassCounter extends React.Component<Props, State> { readonly state: State = { count: 0, }; handleIncrement = () => { this.setState({ count: this.state.count + 1 }); }; render() { const { handleIncrement } = this; const { label } = this.props; const { count } = this.state; return ( <div> <span> {label}: {count} </span> <button type="button" onClick={handleIncrement}> {`Increment`} </button> </div> ); } }
import * as React from 'react'; type Props = { label: string; initialCount: number; }; type State = { count: number; }; export class ClassCounterWithDefaultProps extends React.Component< Props, State > { static defaultProps = { initialCount: 0, }; readonly state: State = { count: this.props.initialCount, }; handleIncrement = () => { this.setState({ count: this.state.count + 1 }); }; render() { const { handleIncrement } = this; const { label } = this.props; const { count } = this.state; return ( <div> <span> {label}: {count} </span> <button type="button" onClick={handleIncrement}> {`Increment`} </button> </div> ); } }
import * as React from 'react'; export interface GenericListProps<T> { items: T[]; itemRenderer: (item: T) => JSX.Element; } export class GenericList<T> extends React.Component<GenericListProps<T>, {}> { render() { const { items, itemRenderer } = this.props; return ( <div> {items.map(itemRenderer)} </div> ); } }
https://zh-hans.reactjs.org/d...
將 children 用做 render prop 的簡單組件
import * as React from 'react'; interface NameProviderProps { children: (state: NameProviderState) => React.ReactNode; } interface NameProviderState { readonly name: string; } export class NameProvider extends React.Component<NameProviderProps, NameProviderState> { readonly state: NameProviderState = { name: 'Piotr' }; render() { return this.props.children(this.state); } }
Mouse
組件的例子來源於
Render Props - React 文檔
import * as React from 'react'; export interface MouseProviderProps { render: (state: MouseProviderState) => React.ReactNode; } interface MouseProviderState { readonly x: number; readonly y: number; } export class MouseProvider extends React.Component<MouseProviderProps, MouseProviderState> { readonly state: MouseProviderState = { x: 0, y: 0 }; handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => { this.setState({ x: event.clientX, y: event.clientY, }); }; render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */} {this.props.render(this.state)} </div> ); } }
https://zh-hans.reactjs.org/d...
給無狀態的計數器加上狀態
import React from 'react'; import { Diff } from 'utility-types'; // These props will be injected into the base component interface InjectedProps { count: number; onIncrement: () => void; } export const withState = <BaseProps extends InjectedProps>( BaseComponent: React.ComponentType<BaseProps> ) => { type HocProps = Diff<BaseProps, InjectedProps> & { // here you can extend hoc with new props initialCount?: number; }; type HocState = { readonly count: number; }; return class Hoc extends React.Component<HocProps, HocState> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withState(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; readonly state: HocState = { count: Number(this.props.initialCount) || 0, }; handleIncrement = () => { this.setState({ count: this.state.count + 1 }); }; render() { const { ...restProps } = this.props; const { count } = this.state; return ( <BaseComponent count={count} // injected onIncrement={this.handleIncrement} // injected {...(restProps as BaseProps)} /> ); } }; };
import * as React from 'react'; import { withState } from '../hoc'; import { FCCounter } from '../components'; const FCCounterWithState = withState(FCCounter); export default () => <FCCounterWithState label={'FCCounterWithState'} />;
用 componentDidCatch 給任意組件加上錯誤處理功能
import React from 'react'; const MISSING_ERROR = 'Error was swallowed during propagation.'; export const withErrorBoundary = <BaseProps extends {}>( BaseComponent: React.ComponentType<BaseProps> ) => { type HocProps = { // here you can extend hoc with new props }; type HocState = { readonly error: Error | null | undefined; }; return class Hoc extends React.Component<HocProps, HocState> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withErrorBoundary(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; readonly state: HocState = { error: undefined, }; componentDidCatch(error: Error | null, info: object) { this.setState({ error: error || new Error(MISSING_ERROR) }); this.logErrorToCloud(error, info); } logErrorToCloud = (error: Error | null, info: object) => { // TODO: send error report to service provider }; render() { const { children, ...restProps } = this.props; const { error } = this.state; if (error) { return <BaseComponent {...(restProps as BaseProps)} />; } return children; } }; };
import React, {useState} from 'react'; import { withErrorBoundary } from '../hoc'; import { ErrorMessage } from '../components'; const ErrorMessageWithErrorBoundary = withErrorBoundary(ErrorMessage); const BrokenComponent = () => { throw new Error('I\'m broken! Don\'t render me.'); }; const BrokenButton = () => { const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] = useState(false); if (shouldRenderBrokenComponent) { return <BrokenComponent />; } return ( <button type="button" onClick={() => { setShouldRenderBrokenComponent(true); }} > {`Throw nasty error`} </button> ); }; export default () => ( <ErrorMessageWithErrorBoundary> <BrokenButton /> </ErrorMessageWithErrorBoundary> );
用 componentDidCatch 給任意組件加上錯誤處理功能
import { RootState } from 'MyTypes'; import React from 'react'; import { connect } from 'react-redux'; import { Diff } from 'utility-types'; import { countersActions, countersSelectors } from '../features/counters'; // These props will be injected into the base component interface InjectedProps { count: number; onIncrement: () => void; } export const withConnectedCount = <BaseProps extends InjectedProps>( BaseComponent: React.ComponentType<BaseProps> ) => { const mapStateToProps = (state: RootState) => ({ count: countersSelectors.getReduxCounter(state.counters), }); const dispatchProps = { onIncrement: countersActions.increment, }; type HocProps = ReturnType<typeof mapStateToProps> & typeof dispatchProps & { // here you can extend ConnectedHoc with new props overrideCount?: number; }; class Hoc extends React.Component<HocProps> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withConnectedCount(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; render() { const { count, onIncrement, overrideCount, ...restProps } = this.props; return ( <BaseComponent count={overrideCount || count} // injected onIncrement={onIncrement} // injected {...(restProps as BaseProps)} /> ); } } const ConnectedHoc = connect< ReturnType<typeof mapStateToProps>, typeof dispatchProps, // use "undefined" if NOT using dispatchProps Diff<BaseProps, InjectedProps>, RootState >( mapStateToProps, dispatchProps )(Hoc); return ConnectedHoc; };
import * as React from 'react'; import { withConnectedCount } from '../hoc'; import { FCCounter } from '../components'; const FCCounterWithConnectedCount = withConnectedCount(FCCounter); export default () => ( <FCCounterWithConnectedCount overrideCount={5} label={'FCCounterWithState'} /> );
import Types from 'MyTypes'; import { connect } from 'react-redux'; import { countersActions, countersSelectors } from '../features/counters'; import { FCCounter } from '../components'; const mapStateToProps = (state: Types.RootState) => ({ count: countersSelectors.getReduxCounter(state.counters), }); const dispatchProps = { onIncrement: countersActions.increment, }; export const FCCounterConnected = connect( mapStateToProps, dispatchProps )(FCCounter);
import * as React from 'react'; import { FCCounterConnected } from '.'; export default () => <FCCounterConnected label={'FCCounterConnected'} />;
import Types from 'MyTypes'; import { connect } from 'react-redux'; import { countersActions, countersSelectors } from '../features/counters'; import { FCCounter } from '../components'; type OwnProps = { initialCount?: number; }; const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) => ({ count: countersSelectors.getReduxCounter(state.counters) + (ownProps.initialCount || 0), }); const dispatchProps = { onIncrement: countersActions.increment, }; export const FCCounterConnectedOwnProps = connect( mapStateToProps, dispatchProps )(FCCounter);
import * as React from 'react'; import { FCCounterConnectedOwnProps } from '.'; export default () => ( <FCCounterConnectedOwnProps label={'FCCounterConnectedOwnProps'} initialCount={10} /> );
redux-thunk
import Types from 'MyTypes'; import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; import * as React from 'react'; import { countersActions } from '../features/counters'; // Thunk Action const incrementWithDelay = () => async (dispatch: Dispatch): Promise<void> => { setTimeout(() => dispatch(countersActions.increment()), 1000); }; const mapStateToProps = (state: Types.RootState) => ({ count: state.counters.reduxCounter, }); const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) => bindActionCreators( { onIncrement: incrementWithDelay, }, dispatch ); type Props = ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & { label: string; }; export const FCCounter: React.FC<Props> = props => { const { label, count, onIncrement } = props; const handleIncrement = () => { // Thunk action is correctly typed as promise onIncrement().then(() => { // ... }); }; return ( <div> <span> {label}: {count} </span> <button type="button" onClick={handleIncrement}> {`Increment`} </button> </div> ); }; export const FCCounterConnectedBindActionCreators = connect( mapStateToProps, mapDispatchToProps )(FCCounter);
import * as React from 'react'; import { FCCounterConnectedBindActionCreators } from '.'; export default () => ( <FCCounterConnectedBindActionCreators label={'FCCounterConnectedBindActionCreators'} /> );
https://zh-hans.reactjs.org/d...
import * as React from 'react'; export type Theme = React.CSSProperties; type Themes = { dark: Theme; light: Theme; }; export const themes: Themes = { dark: { color: 'black', backgroundColor: 'white', }, light: { color: 'white', backgroundColor: 'black', }, }; export type ThemeContextProps = { theme: Theme; toggleTheme?: () => void }; const ThemeContext = React.createContext<ThemeContextProps>({ theme: themes.light }); export default ThemeContext;
import React from 'react'; import ThemeContext, { themes, Theme } from './theme-context'; import ToggleThemeButton from './theme-consumer'; interface State { theme: Theme; } export class ThemeProvider extends React.Component<{}, State> { readonly state: State = { theme: themes.light }; toggleTheme = () => { this.setState(state => ({ theme: state.theme === themes.light ? themes.dark : themes.light, })); } render() { const { theme } = this.state; const { toggleTheme } = this; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> <ToggleThemeButton /> </ThemeContext.Provider> ); } }
import * as React from 'react'; import ThemeContext from './theme-context'; type Props = {}; export default function ToggleThemeButton(props: Props) { return ( <ThemeContext.Consumer> {({ theme, toggleTheme }) => <button style={theme} onClick={toggleTheme} {...props} />} </ThemeContext.Consumer> ); }
import * as React from 'react'; import ThemeContext from './theme-context'; type Props = {}; export class ToggleThemeButtonClass extends React.Component<Props> { static contextType = ThemeContext; context!: React.ContextType<typeof ThemeContext>; render() { const { theme, toggleTheme } = this.context; return ( <button style={theme} onClick={toggleTheme}> Toggle Theme </button> ); } }
https://zh-hans.reactjs.org/d...
https://zh-hans.reactjs.org/d...
import * as React from 'react'; type Props = { initialCount: number }; export default function Counter({initialCount}: Props) { const [count, setCount] = React.useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </> ); }
用於函數組件的狀態管理 Hook (相似 Redux)。
import * as React from 'react'; interface State { count: number; } type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; case 'reset': return { count: 0 }; default: throw new Error(); } } interface CounterProps { initialCount: number; } function Counter({ initialCount }: CounterProps) { const [state, dispatch] = React.useReducer(reducer, { count: initialCount, }); return ( <> Count: {state.count} <button onClick={() => dispatch({ type: 'reset' })}>Reset</button> <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </> ); } export default Counter;
https://zh-hans.reactjs.org/d...
import * as React from 'react'; import ThemeContext from '../context/theme-context'; type Props = {}; export default function ThemeToggleButton(props: Props) { const { theme, toggleTheme } = React.useContext(ThemeContext); return ( <button onClick={toggleTheme} style={theme} > Toggle Theme </button> ); }
RootState
- 表示根 state 樹的類型能夠做爲 import,使用 Redux connect
方法鏈接組件時,可以確保類型安全性
RootAction
- 表示全部 action 對象集合的類型能夠做爲 import,用於不一樣層次中(reducers, sagas 或 redux-observables epics)接收和發送 redux actions
import { StateType, ActionType } from 'typesafe-actions'; declare module 'MyTypes' { export type Store = StateType<typeof import('./index').default>; export type RootAction = ActionType<typeof import('./root-action').default>; export type RootState = StateType<ReturnType<typeof import('./root-reducer').default>>; } declare module 'typesafe-actions' { interface Types { RootAction: ActionType<typeof import('./root-action').default>; } }
當建立 store 實例時,咱們不須要編寫任何額外的類型,它會經過類型推斷自動創建一個類型安全的 Store 實例。
生成的 store 實例中的方法(像getState
和dispatch
)將支持類型檢查,並可以暴露全部的類型錯誤。
import { RootAction, RootState, Services } from 'MyTypes'; import { createStore, applyMiddleware } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; import { createBrowserHistory } from 'history'; import { routerMiddleware as createRouterMiddleware } from 'connected-react-router'; import { composeEnhancers } from './utils'; import rootReducer from './root-reducer'; import rootEpic from './root-epic'; import services from '../services'; // browser history export const history = createBrowserHistory(); export const epicMiddleware = createEpicMiddleware< RootAction, RootAction, RootState, Services >({ dependencies: services, }); const routerMiddleware = createRouterMiddleware(history); // configure middlewares const middlewares = [epicMiddleware, routerMiddleware]; // compose enhancers const enhancer = composeEnhancers(applyMiddleware(...middlewares)); // rehydrate state on app start const initialState = {}; // create store const store = createStore(rootReducer(history), initialState, enhancer); epicMiddleware.run(rootEpic); // export store singleton instance export default store;
咱們將使用成熟的輔助庫typesafe-actions
![]()
它被設計成便於使用 TypeScript 來寫 Redux。
查看這個進階教程來學習更多:Typesafe-Actions - Tutorial!
下面的方案用一個簡單的工廠函數來自動建立類型安全的 action creators。目的是減小重複的 actions 和 creators 類型聲明代碼,並減小代碼維護工做。生成結果是絕對類型安全的 action-creators 及其 actions。
/* eslint-disable */ import { action } from 'typesafe-actions'; import { ADD, INCREMENT } from './constants'; /* SIMPLE API */ export const increment = () => action(INCREMENT); export const add = (amount: number) => action(ADD, amount); /* ADVANCED API */ // More flexible allowing to create complex actions more easily // use can use "action-creator" instance in place of "type constant" // e.g. case getType(increment): return action.payload; // This will allow to completely eliminate need for "constants" in your application, more info here: // https://github.com/piotrwitek/typesafe-actions#constants import { createAction } from 'typesafe-actions'; import { Todo } from '../todos/models'; export const emptyAction = createAction(INCREMENT)<void>(); export const payloadAction = createAction(ADD)<number>(); export const payloadMetaAction = createAction(ADD)<number, string>(); export const payloadCreatorAction = createAction( 'TOGGLE_TODO', (todo: Todo) => todo.id )<string>();
import store from '../../store'; import { countersActions as counter } from '../counters'; // store.dispatch(counter.increment(1)); // Error: Expected 0 arguments, but got 1. store.dispatch(counter.increment()); // OK // store.dispatch(counter.add()); // Error: Expected 1 arguments, but got 0. store.dispatch(counter.add(1)); // OK
用 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
是淺比較的)
小貼士: 使用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
Readonly
的遞歸版本是 DeepReadonly
爲了解決上述問題,咱們可使用 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
爲了理解下一小節,請確保瞭解 類型推論, 基於控制流的類型分析 以及 標記聯合類型
import { combineReducers } from 'redux'; import { ActionType } from 'typesafe-actions'; import { Todo, TodosFilter } from './models'; import * as actions from './actions'; import { ADD, CHANGE_FILTER, TOGGLE } from './constants'; export type TodosAction = ActionType<typeof actions>; export type TodosState = Readonly<{ todos: Todo[]; todosFilter: TodosFilter; }>; const initialState: TodosState = { todos: [], todosFilter: TodosFilter.All, }; export default combineReducers<TodosState, TodosAction>({ todos: (state = initialState.todos, action) => { switch (action.type) { case ADD: return [...state, action.payload]; case TOGGLE: return state.map(item => item.id === action.payload ? { ...item, completed: !item.completed } : item ); default: return state; } }, todosFilter: (state = initialState.todosFilter, action) => { switch (action.type) { case CHANGE_FILTER: return action.payload; default: return state; } }, });
typesafe-actions
進行 reducer 類型聲明請注意,咱們不須要在 API 上使用任何泛型類型參數。能夠和傳統的 reducer 寫法進行比較,它們是等價的。
import { combineReducers } from 'redux'; import { createReducer } from 'typesafe-actions'; import { Todo, TodosFilter } from './models'; import { ADD, CHANGE_FILTER, TOGGLE } from './constants'; export type TodosState = Readonly<{ todos: Todo[]; todosFilter: TodosFilter; }>; const initialState: TodosState = { todos: [], todosFilter: TodosFilter.All, }; const todos = createReducer(initialState.todos) .handleType(ADD, (state, action) => [...state, action.payload]) .handleType(TOGGLE, (state, action) => state.map(item => item.id === action.payload ? { ...item, completed: !item.completed } : item ) ); const todosFilter = createReducer(initialState.todosFilter).handleType( CHANGE_FILTER, (state, action) => action.payload ); export default combineReducers({ todos, todosFilter, });
import { todosReducer as reducer, todosActions as actions, TodosState, } from './'; /** * FIXTURES */ const getInitialState = (initial?: Partial<TodosState>) => reducer(initial as TodosState, {} as any); /** * STORIES */ describe('Todos Stories', () => { describe('initial state', () => { it('should match a snapshot', () => { const initialState = getInitialState(); expect(initialState).toMatchSnapshot(); }); }); describe('adding todos', () => { it('should add a new todo as the first element', () => { const initialState = getInitialState(); expect(initialState.todos).toHaveLength(0); const state = reducer(initialState, actions.add('new todo')); expect(state.todos).toHaveLength(1); expect(state.todos[0].title).toEqual('new todo'); }); }); describe('toggling completion state', () => { it('should mark active todo as complete', () => { const activeTodo = { id: '1', completed: false, title: 'active todo' }; const initialState = getInitialState({ todos: [activeTodo] }); expect(initialState.todos[0].completed).toBeFalsy(); const state1 = reducer(initialState, actions.toggle(activeTodo.id)); expect(state1.todos[0].completed).toBeTruthy(); }); }); });
redux-observable
編寫異步流import { RootAction, RootState, Services } from 'MyTypes'; import { Epic } from 'redux-observable'; import { tap, ignoreElements, filter } from 'rxjs/operators'; import { isOfType } from 'typesafe-actions'; import { todosConstants } from '../todos'; // contrived example!!! export const logAddAction: Epic<RootAction, RootAction, RootState, Services> = ( action$, state$, { logger } ) => action$.pipe( filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; } tap(action => { logger.log( `action type must be equal: ${todosConstants.ADD} === ${action.type}` ); }), ignoreElements() );
import { StateObservable, ActionsObservable } from 'redux-observable'; import { RootState, Services, RootAction } from 'MyTypes'; import { Subject } from 'rxjs'; import { add } from './actions'; import { logAddAction } from './epics'; // Simple typesafe mock of all the services, you dont't need to mock anything else // It is decoupled and reusable for all your tests, just put it in a separate file const services = { logger: { log: jest.fn<Services['logger']['log']>(), }, localStorage: { loadState: jest.fn<Services['localStorage']['loadState']>(), saveState: jest.fn<Services['localStorage']['saveState']>(), }, }; describe('Todos Epics', () => { let state$: StateObservable<RootState>; beforeEach(() => { state$ = new StateObservable<RootState>( new Subject<RootState>(), undefined as any ); }); describe('logging todos actions', () => { beforeEach(() => { services.logger.log.mockClear(); }); it('should call the logger service when adding a new todo', done => { const addTodoAction = add('new todo'); const action$ = ActionsObservable.of(addTodoAction); logAddAction(action$, state$, services) .toPromise() .then((outputAction: RootAction) => { expect(services.logger.log).toHaveBeenCalledTimes(1); expect(services.logger.log).toHaveBeenCalledWith( 'action type must be equal: todos/ADD === todos/ADD' ); // expect output undefined because we're using "ignoreElements" in epic expect(outputAction).toEqual(undefined); done(); }); }); }); });
reselect
生成 Selectorsimport { createSelector } from 'reselect'; import { TodosState } from './reducer'; export const getTodos = (state: TodosState) => state.todos; export const getTodosFilter = (state: TodosState) => state.todosFilter; export const getFilteredTodos = createSelector(getTodos, getTodosFilter, (todos, todosFilter) => { switch (todosFilter) { case 'completed': return todos.filter(t => t.completed); case 'active': return todos.filter(t => !t.completed); default: return todos; } });
react-redux
的 connect 方法__注意__:在下面一段代碼中,只有關於 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);
redux-thunk
__注意__:使用 thunk action creators 時你須要使用 bindActionCreators
。只有這樣,你才能得到正確的 dispatch props 類型簽名,以下所示。
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
方便地把它添加到你的項目裏。
{ "include": [ "src", "typings" ], "exclude": [ "src/**/*.spec.*" ], "extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json", "compilerOptions": {} }
https://www.npmjs.com/package...
這個庫經過把運行時輔助函數外置化,而不是內嵌到每一個文件中,來減小你的打包文件大小。
安裝
npm i tslib
把這行加到你的 tsconfig.json
中:
"compilerOptions": { "importHelpers": true }
https://palantir.github.io/ts...
安裝
npm i -D tslint
若是用於 React 項目,你應該加上額外的react
規則集:npm i -D tslint-react
https://github.com/palantir/t...
咱們有推薦配置文件,你能夠藉助 react-redux-typescript-scripts
方便地把它添加到你的項目裏。
{ "extends": [ "./node_modules/react-redux-typescript-scripts/tslint.json", "./node_modules/react-redux-typescript-scripts/tslint-react.json" ], "rules": { // you can further customize options here } }
https://eslint.org/
https://typescript-eslint.io
安裝
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
咱們有推薦配置文件,他會自動添加 TypeScript 的解析器和插件,你能夠藉助 react-redux-typescript-scripts
方便地把它添加到你的項目裏。
{ "extends": [ "react-app", "./node_modules/react-redux-typescript-scripts/eslint.js" ], "rules": { // you can further customize options here } }
安裝
npm i -D jest ts-jest @types/jest
{ "verbose": true, "transform": { ".(ts|tsx)": "ts-jest" }, "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$", "moduleFileExtensions": ["ts", "tsx", "js"], "moduleNameMapper": { "^Components/(.*)": "./src/components/$1" }, "globals": { "window": {}, "ts-jest": { "tsConfig": "./tsconfig.json" } }, "setupFiles": ["./jest.stubs.js"], "testURL": "http://localhost/" }
// Global/Window object Stubs for Jest window.matchMedia = window.matchMedia || function () { return { matches: false, addListener: function () { }, removeListener: function () { }, }; }; window.requestAnimationFrame = function (callback) { setTimeout(callback); }; window.localStorage = { getItem: function () { }, setItem: function () { }, }; Object.values = () => [];
不。用了 TypeScript 以後,沒有必要再使用 PropTypes。當聲明 Props 和 State 接口後,你將經過靜態類型檢查得到徹底的自動補全和編碼時的安全性。這樣,你就能直接避免運行時錯誤,並減小大量調試時間。額外的好處是,這也是一種用於在源碼中解釋組件公共 API 的優雅而標準化的方法。
interface
聲明,何時使用 type
別名?從實際來看,使用 interface
聲明在編譯錯誤時會生成一個 interface 同名標識,相反 type
別名不會生成標識名,而且會展開顯示全部屬性和嵌套的類型。
儘管我大部分時候更喜歡用 type
,可是有時候編譯錯誤過於冗長影響排查,我會根據二者的差異,改用 interface 來隱藏報錯中沒那麼重要的類型細節。
相關的 ts-lint
規則:https://palantir.github.io/ts...
一個常見的適應性方案是使用文件夾模塊模式,這樣你能夠根據狀況同時使用具名和默認 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 禁用該模塊的類型檢查。
// typings/modules.d.ts declare module 'MyTypes'; declare module 'react-test-renderer';
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 "antd@2.10.0" npm package declare module '../node_modules/antd/lib/input/Input' { export interface InputProps { autoFocus?: boolean; } }
// fixed broken public type-definitions in "rxjs@5.4.1" 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 官方文檔 找到
相關進階教程精選清單
高階組件: