"這個指南是一個最新的摘要,記錄了關於如何用TypeScript 以函數式風格使用React(以及相關生態)最重要的模式和示例。它會使你的代碼在從具體實現中進行類型推導時絕對是類型安全的,這樣就能減小來自過分類型聲明的信息噪音,並更容易寫出易於長期維護的正確類型聲明。"javascript
--strict
模式),而且在嚮應用的下游代碼傳遞時,不會丟失類型信息(好比:缺乏類型斷言或用 any
來強行使用)🌟 - 新內容及更新板塊css
React - 類型定義速查表html
React.CSSProperties
typescript
- 配置和開發者工具
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>
);
}
}
複製代碼
將 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> ); } } 複製代碼
給無狀態的計數器加上狀態
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'} />
);
複製代碼
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>
);
}
}
複製代碼
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;
複製代碼
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": {}
}
複製代碼
這個庫經過把運行時輔助函數外置化,而不是內嵌到每一個文件中,來減小你的打包文件大小。
安裝
npm i tslib
複製代碼
把這行加到你的 tsconfig.json
中:
"compilerOptions": {
"importHelpers": true
}
複製代碼
安裝
npm i -D tslint
複製代碼
若是用於 React 項目,你應該加上額外的
react
規則集:npm i -D tslint-react
github.com/palantir/ts…
咱們有推薦配置文件,你能夠藉助 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
}
}
複製代碼
eslint.org/ 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
規則:palantir.github.io/tslint/rule…
一個常見的適應性方案是使用文件夾模塊模式,這樣你能夠根據狀況同時使用具名和默認 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 官方文檔 找到
相關進階教程精選清單
高階組件: