Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

The complete guide to static typing in "React & Redux" apps using TypeScript

License

NotificationsYou must be signed in to change notification settings

piotrwitek/react-redux-typescript-guide

Repository files navigation

"This guide is aliving compendium documenting the most important patterns and recipes on how to useReact (and its Ecosystem) in afunctional style usingTypeScript. It will help you make your codecompletely type-safe while focusing oninferring the types from implementation so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."

Join the community on SpectrumJoin the chat at https://gitter.im/react-redux-typescript-guide/Lobby

Found it useful? Want more updates?

Show your support by giving a ⭐

Buy Me a CoffeeBecome a Patron



What's new?

🎉Now updated to supportTypeScript v4.6 🎉🚀 _Updated totypesafe-actions@5.x 🚀



Goals

  • Complete type safety (with--strict flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking withany type)
  • Make type annotations concise by eliminating redundancy in types using advanced TypeScript Language features likeType Inference andControl flow analysis
  • Reduce repetition and complexity of types with TypeScript focusedcomplementary libraries

React, Redux, Typescript Ecosystem

  • typesafe-actions - Typesafe utilities for "action-creators" in Redux / Flux Architecture
  • utility-types - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types.
  • react-redux-typescript-scripts - dev-tools configuration files shared between projects based on this guide

Examples

Playground Project

Build Status

Check out our Playground Project located in the/playground folder. It contains all source files of the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type-definitions (like@types/react or@types/react-redux) to ensure the examples are up-to-date and not broken with updated definitions (It's based oncreate-react-app --typescript).

Playground project was created so that you can simply clone the repository locally and immediately play around with all the component patterns found in the guide. It will help you to learn all the examples from this guide in a real project environment without the need to create complicated environment setup by yourself.

Contributing Guide

You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide:CONTRIBUTING.md

Funding

You can also help by funding issues.Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.

I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.

Let's fund issues in this repository


🌟 -New or updated section

Table of Contents


Installation

Types for 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

*NB: Guide is based on types for Redux >= v4.x.x.

⇧ back to top


React Types Cheatsheet

React.FC<Props> |React.FunctionComponent<Props>

Type representing a functional component

constMyComponent:React.FC<Props>= ...

React.Component<Props, State>

Type representing a class component

classMyComponentextendsReact.Component<Props,State>{ ...

React.ComponentType<Props>

Type representing union of (React.FC<Props> | React.Component<Props>) - used in HOC

constwithState=<PextendsWrappedComponentProps>(WrappedComponent:React.ComponentType<P>,)=>{ ...

React.ComponentProps<typeof XXX>

Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props)

typeMyComponentProps=React.ComponentProps<typeofMyComponent>;

React.ReactElement |JSX.Element

Type representing a concept of React Element - representation of a native DOM component (e.g.<div />), or a user-defined composite component (e.g.<MyComponent />)

constelementOnly:React.ReactElement=<div/>||<MyComponent/>;

React.ReactNode

Type representing any possible type of React node (basically ReactElement (including Fragments and Portals) + primitive JS types)

constelementOrPrimitive:React.ReactNode='string'||0||false||null||undefined||<div/>||<MyComponent/>;constComponent=({children:React.ReactNode})=> ...

React.CSSProperties

Type representing style object in JSX - for css-in-js styles

conststyles:React.CSSProperties={flexDirection:'row', ...constelement=<divstyle={styles}...

React.XXXHTMLAttributes<HTMLXXXElement>

Type representing HTML attributes of specified HTML Element - for extending HTML Elements

constInput:React.FC<Props&React.InputHTMLAttributes<HTMLInputElement>>=props=>{ ...}<Inputabout={...}accept={...}alt={...} .../>

React.ReactEventHandler<HTMLXXXElement>

Type representing generic event handler - for declaring event handlers

consthandleChange:React.ReactEventHandler<HTMLInputElement>=(ev)=>{ ...}<inputonChange={handleChange}.../>

React.XXXEvent<HTMLXXXElement>

Type representing more specific event. Some common event examples:ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent.

consthandleChange=(ev:React.MouseEvent<HTMLDivElement>)=>{ ...}<divonMouseMove={handleChange}.../>

In code aboveReact.MouseEvent<HTMLDivElement> is type of mouse event, and this event happened onHTMLDivElement

⇧ back to top


React

Function Components - FC

- Counter Component

import*asReactfrom'react';typeProps={label:string;count:number;onIncrement:()=>void;};exportconstFCCounter:React.FC<Props>=props=>{const{ label, count, onIncrement}=props;consthandleIncrement=()=>{onIncrement();};return(<div><span>{label}:{count}</span><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);};

⟩⟩⟩ demo

⇧ back to top

- Counter Component with default props

import*asReactfrom'react';typeProps={label:string;count:number;onIncrement:()=>void;};// React.FC is unaplicable here due not working properly with default props// https://github.com/facebook/create-react-app/pull/8177exportconstFCCounterWithDefaultProps=(props:Props):JSX.Element=>{const{ label, count, onIncrement}=props;consthandleIncrement=()=>{onIncrement();};return(<div><span>{label}:{count}</span><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);};FCCounterWithDefaultProps.defaultProps={count:5};

⟩⟩⟩ demo

⇧ back to top

-Spreading attributes in Component

import*asReactfrom'react';typeProps=React.PropsWithChildren<{className?:string;style?:React.CSSProperties;}>;exportconstFCSpreadAttributes:React.FC<Props>=(props)=>{const{ children, ...restProps}=props;return<div{...restProps}>{children}</div>;};

⟩⟩⟩ demo

⇧ back to top


Class Components

- Class Counter Component

import*asReactfrom'react';typeProps={label:string;};typeState={count:number;};exportclassClassCounterextendsReact.Component<Props,State>{readonlystate: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><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);}}

⟩⟩⟩ demo

⇧ back to top

- Class Component with default props

import*asReactfrom'react';typeProps={label:string;initialCount:number;};typeState={count:number;};exportclassClassCounterWithDefaultPropsextendsReact.Component<Props,State>{staticdefaultProps={initialCount:0,};readonlystate: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><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);}}

⟩⟩⟩ demo

⇧ back to top


Generic Components

  • easily create typed component variations and reuse common logic
  • common use case is a generic list components

- Generic List Component

import*asReactfrom'react';exportinterfaceGenericListProps<T>{items:T[];itemRenderer:(item:T)=>JSX.Element;}exportclassGenericList<T>extendsReact.Component<GenericListProps<T>,{}>{render(){const{ items, itemRenderer}=this.props;return(<div>{items.map(itemRenderer)}</div>);}}

⟩⟩⟩ demo

⇧ back to top


Hooks

https://reactjs.org/docs/hooks-intro.html

- useState

https://reactjs.org/docs/hooks-reference.html#usestate

import*asReactfrom'react';typeProps={initialCount:number};exportdefaultfunctionCounter({initialCount}:Props){const[count,setCount]=React.useState(initialCount);return(<>      Count:{count}<buttononClick={()=>setCount(initialCount)}>Reset</button><buttononClick={()=>setCount(prevCount=>prevCount+1)}>+</button><buttononClick={()=>setCount(prevCount=>prevCount-1)}>-</button></>);}

⇧ back to top

- useContext

https://reactjs.org/docs/hooks-reference.html#usecontext

import*asReactfrom'react';importThemeContextfrom'../context/theme-context';typeProps={};exportdefaultfunctionThemeToggleButton(props:Props){const{ theme, toggleTheme}=React.useContext(ThemeContext);return(<buttononClick={toggleTheme}style={theme}>      Toggle Theme</button>);}

⇧ back to top

- useReducer

https://reactjs.org/docs/hooks-reference.html#usereducer

import*asReactfrom'react';interfaceState{count:number;}typeAction={type:'reset'}|{type:'increment'}|{type:'decrement'};functionreducer(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:thrownewError();}}interfaceCounterProps{initialCount:number;}functionCounter({ initialCount}:CounterProps){const[state,dispatch]=React.useReducer(reducer,{count:initialCount,});return(<>      Count:{state.count}<buttononClick={()=>dispatch({type:'reset'})}>Reset</button><buttononClick={()=>dispatch({type:'increment'})}>+</button><buttononClick={()=>dispatch({type:'decrement'})}>-</button></>);}exportdefaultCounter;

⇧ back to top


Render Props

https://reactjs.org/docs/render-props.html

- Name Provider Component

Simple component using children as a render prop

import*asReactfrom'react';interfaceNameProviderProps{children:(state:NameProviderState)=>React.ReactNode;}interfaceNameProviderState{readonlyname:string;}exportclassNameProviderextendsReact.Component<NameProviderProps,NameProviderState>{readonlystate:NameProviderState={name:'Piotr'};render(){returnthis.props.children(this.state);}}

⟩⟩⟩ demo

⇧ back to top

- Mouse Provider Component

Mouse component found inRender Props React Docs

import*asReactfrom'react';exportinterfaceMouseProviderProps{render:(state:MouseProviderState)=>React.ReactNode;}interfaceMouseProviderState{readonlyx:number;readonlyy:number;}exportclassMouseProviderextendsReact.Component<MouseProviderProps,MouseProviderState>{readonlystate:MouseProviderState={x:0,y:0};handleMouseMove=(event:React.MouseEvent<HTMLDivElement>)=>{this.setState({x:event.clientX,y:event.clientY,});};render(){return(<divstyle={{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>);}}

⟩⟩⟩ demo

⇧ back to top


Higher-Order Components

https://reactjs.org/docs/higher-order-components.html

- HOC wrapping a component

Adds state to a stateless counter

importReactfrom'react';import{Diff}from'utility-types';// These props will be injected into the base componentinterfaceInjectedProps{count:number;onIncrement:()=>void;}exportconstwithState=<BasePropsextendsInjectedProps>(BaseComponent:React.ComponentType<BaseProps>)=>{typeHocProps=Diff<BaseProps,InjectedProps>&{// here you can extend hoc with new propsinitialCount?:number;};typeHocState={readonlycount:number;};returnclassHocextendsReact.Component<HocProps,HocState>{// Enhance component name for debugging and React-Dev-ToolsstaticdisplayName=`withState(${BaseComponent.name})`;// reference to original wrapped componentstaticreadonlyWrappedComponent=BaseComponent;readonlystate: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{...(restPropsasBaseProps)}count={count}// injectedonIncrement={this.handleIncrement}// injected/>);}};};
Click to expand

import*asReactfrom'react';import{withState}from'../hoc';import{FCCounter}from'../components';constFCCounterWithState=withState(FCCounter);exportdefault()=><FCCounterWithStatelabel={'FCCounterWithState'}/>;

⇧ back to top

- HOC wrapping a component and injecting props

Adds error handling using componentDidCatch to any component

importReactfrom'react';constMISSING_ERROR='Error was swallowed during propagation.';exportconstwithErrorBoundary=<BasePropsextends{}>(BaseComponent:React.ComponentType<BaseProps>)=>{typeHocProps=React.PropsWithChildren<{// here you can extend hoc with new props}>;typeHocState={readonlyerror:Error|null|undefined;};returnclassHocextendsReact.Component<HocProps,HocState>{// Enhance component name for debugging and React-Dev-ToolsstaticdisplayName=`withErrorBoundary(${BaseComponent.name})`;// reference to original wrapped componentstaticreadonlyWrappedComponent=BaseComponent;readonlystate:HocState={error:undefined,};componentDidCatch(error:Error|null,info:object){this.setState({error:error||newError(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{...(restPropsasBaseProps)}/>;}returnchildren;}};};
Click to expand

importReact,{useState}from'react';import{withErrorBoundary}from'../hoc';import{ErrorMessage}from'../components';constErrorMessageWithErrorBoundary=withErrorBoundary(ErrorMessage);constBrokenComponent=()=>{thrownewError('I\'m broken! Don\'t render me.');};constBrokenButton=()=>{const[shouldRenderBrokenComponent,setShouldRenderBrokenComponent]=useState(false);if(shouldRenderBrokenComponent){return<BrokenComponent/>;}return(<buttontype="button"onClick={()=>{setShouldRenderBrokenComponent(true);}}>{`Throw nasty error`}</button>);};exportdefault()=>(<ErrorMessageWithErrorBoundary><BrokenButton/></ErrorMessageWithErrorBoundary>);

⇧ back to top

- Nested HOC - wrapping a component, injecting props and connecting to redux 🌟

Adds error handling using componentDidCatch to any component

import{RootState}from'MyTypes';importReactfrom'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 componentinterfaceInjectedProps{count:number;onIncrement:()=>void;}exportconstwithConnectedCount=<BasePropsextendsInjectedProps>(BaseComponent:React.ComponentType<BaseProps>)=>{constmapStateToProps=(state:RootState)=>({count:countersSelectors.getReduxCounter(state.counters),});constdispatchProps={onIncrement:countersActions.increment,};typeHocProps=ReturnType<typeofmapStateToProps>&typeofdispatchProps&{// here you can extend ConnectedHoc with new propsoverrideCount?:number;};classHocextendsReact.Component<HocProps>{// Enhance component name for debugging and React-Dev-ToolsstaticdisplayName=`withConnectedCount(${BaseComponent.name})`;// reference to original wrapped componentstaticreadonlyWrappedComponent=BaseComponent;render(){const{ count, onIncrement, overrideCount, ...restProps}=this.props;return(<BaseComponent{...(restPropsasBaseProps)}count={overrideCount||count}// injectedonIncrement={onIncrement}// injected/>);}}constConnectedHoc=connect<ReturnType<typeofmapStateToProps>,typeofdispatchProps,// use "undefined" if NOT using dispatchPropsDiff<BaseProps,InjectedProps>,RootState>(mapStateToProps,dispatchProps)(Hoc);returnConnectedHoc;};
Click to expand

import*asReactfrom'react';import{withConnectedCount}from'../hoc';import{FCCounter}from'../components';constFCCounterWithConnectedCount=withConnectedCount(FCCounter);exportdefault()=>(<FCCounterWithConnectedCountoverrideCount={5}label={'FCCounterWithState'}/>);

⇧ back to top


Redux Connected Components

- Redux connected counter

importTypesfrom'MyTypes';import{connect}from'react-redux';import{countersActions,countersSelectors}from'../features/counters';import{FCCounter}from'../components';constmapStateToProps=(state:Types.RootState)=>({count:countersSelectors.getReduxCounter(state.counters),});constdispatchProps={onIncrement:countersActions.increment,};exportconstFCCounterConnected=connect(mapStateToProps,dispatchProps)(FCCounter);
Click to expand

import*asReactfrom'react';import{FCCounterConnected}from'.';exportdefault()=><FCCounterConnectedlabel={'FCCounterConnected'}/>;

⇧ back to top

- Redux connected counter with own props

importTypesfrom'MyTypes';import{connect}from'react-redux';import{countersActions,countersSelectors}from'../features/counters';import{FCCounter}from'../components';typeOwnProps={initialCount?:number;};constmapStateToProps=(state:Types.RootState,ownProps:OwnProps)=>({count:countersSelectors.getReduxCounter(state.counters)+(ownProps.initialCount||0),});constdispatchProps={onIncrement:countersActions.increment,};exportconstFCCounterConnectedOwnProps=connect(mapStateToProps,dispatchProps)(FCCounter);
Click to expand

import*asReactfrom'react';import{FCCounterConnectedOwnProps}from'.';exportdefault()=>(<FCCounterConnectedOwnPropslabel={'FCCounterConnectedOwnProps'}initialCount={10}/>);

⇧ back to top

- Redux connected counter via hooks

import*asReactfrom'react';import{FCCounter}from'../components';import{increment}from'../features/counters/actions';import{useSelector,useDispatch}from'../store/hooks';constFCCounterConnectedHooksUsage:React.FC=()=>{constcounter=useSelector(state=>state.counters.reduxCounter);constdispatch=useDispatch();return<FCCounterlabel="Use selector"count={counter}onIncrement={()=>dispatch(increment())}/>;};exportdefaultFCCounterConnectedHooksUsage;

⇧ back to top

- Redux connected counter withredux-thunk integration

importTypesfrom'MyTypes';import{bindActionCreators,Dispatch}from'redux';import{connect}from'react-redux';import*asReactfrom'react';import{countersActions}from'../features/counters';// Thunk ActionconstincrementWithDelay=()=>async(dispatch:Dispatch):Promise<void>=>{setTimeout(()=>dispatch(countersActions.increment()),1000);};constmapStateToProps=(state:Types.RootState)=>({count:state.counters.reduxCounter,});constmapDispatchToProps=(dispatch:Dispatch<Types.RootAction>)=>bindActionCreators({onIncrement:incrementWithDelay,},dispatch);typeProps=ReturnType<typeofmapStateToProps>&ReturnType<typeofmapDispatchToProps>&{label:string;};exportconstFCCounter:React.FC<Props>=props=>{const{ label, count, onIncrement}=props;consthandleIncrement=()=>{// Thunk action is correctly typed as promiseonIncrement().then(()=>{// ...});};return(<div><span>{label}:{count}</span><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);};exportconstFCCounterConnectedBindActionCreators=connect(mapStateToProps,mapDispatchToProps)(FCCounter);
Click to expand

import*asReactfrom'react';import{FCCounterConnectedBindActionCreators}from'.';exportdefault()=>(<FCCounterConnectedBindActionCreatorslabel={'FCCounterConnectedBindActionCreators'}/>);

⇧ back to top

Context

https://reactjs.org/docs/context.html

ThemeContext

import*asReactfrom'react';exporttypeTheme=React.CSSProperties;typeThemes={dark:Theme;light:Theme;};exportconstthemes:Themes={dark:{color:'black',backgroundColor:'white',},light:{color:'white',backgroundColor:'black',},};exporttypeThemeContextProps={theme:Theme;toggleTheme?:()=>void};constThemeContext=React.createContext<ThemeContextProps>({theme:themes.light});exportdefaultThemeContext;

⇧ back to top

ThemeProvider

importReactfrom'react';importThemeContext,{themes,Theme}from'./theme-context';importToggleThemeButtonfrom'./theme-consumer';interfaceState{theme:Theme;}exportclassThemeProviderextendsReact.Component<{},State>{readonlystate: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.Providervalue={{ theme, toggleTheme}}><ToggleThemeButton/></ThemeContext.Provider>);}}

⇧ back to top

ThemeConsumer

import*asReactfrom'react';importThemeContextfrom'./theme-context';typeProps={};exportdefaultfunctionToggleThemeButton(props:Props){return(<ThemeContext.Consumer>{({ theme, toggleTheme})=><buttonstyle={theme}onClick={toggleTheme}{...props}/>}</ThemeContext.Consumer>);}

ThemeConsumer in class component

import*asReactfrom'react';importThemeContextfrom'./theme-context';typeProps={};exportclassToggleThemeButtonClassextendsReact.Component<Props>{staticcontextType=ThemeContext;declarecontext:React.ContextType<typeofThemeContext>;render(){const{ theme, toggleTheme}=this.context;return(<buttonstyle={theme}onClick={toggleTheme}>        Toggle Theme</button>);}}

Implementation with Hooks

⇧ back to top


Redux

Store Configuration

Create Global Store Types

RootState - type representing root state-tree

Can be imported in connected components to provide type-safety to Reduxconnect function

RootAction - type representing union type of all action objects

Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics

import{StateType,ActionType}from'typesafe-actions';declare module'MyTypes'{exporttypeStore=StateType<typeofimport('./store').default>;exporttypeRootAction=ActionType<typeofimport('./root-action').default>;exporttypeRootState=StateType<ReturnType<typeofimport('./root-reducer').default>>;}declare module'typesafe-actions'{interfaceTypes{RootAction:ActionType<typeofimport('./root-action').default>;}}

⇧ back to top

Create Store

When creating a store instance we don't need to provide any additional types. It will set-up atype-safe Store instance using type inference.

The resulting store instance methods likegetState ordispatch will be type checked and will expose all type errors

import{RootAction,RootState,Services}from'MyTypes';import{applyMiddleware,createStore}from'redux';import{createEpicMiddleware}from'redux-observable';importservicesfrom'../services';import{routerMiddleware}from'./redux-router';importrootEpicfrom'./root-epic';importrootReducerfrom'./root-reducer';import{composeEnhancers}from'./utils';constepicMiddleware=createEpicMiddleware<RootAction,RootAction,RootState,Services>({dependencies:services,});// configure middlewaresconstmiddlewares=[epicMiddleware,routerMiddleware];// compose enhancersconstenhancer=composeEnhancers(applyMiddleware(...middlewares));// rehydrate state on app startconstinitialState={};// create storeconststore=createStore(rootReducer,initialState,enhancer);epicMiddleware.run(rootEpic);// export store singleton instanceexportdefaultstore;

Action Creators 🌟

We'll be using a battle-tested helper librarytypesafe-actionsLatest Stable VersionNPM Downloads that's designed to make it easy and fun working withRedux inTypeScript.

To learn more please check this in-depth tutorial:Typesafe-Actions - Tutorial!

A solution below is using a simple factory function to automate the creation of type-safe action creators. The goal is to decrease maintenance effort and reduce code repetition of type annotations for actions and creators. The result is completely typesafe action-creators and their actions.

/* eslint-disable */import{action}from'typesafe-actions';import{ADD,INCREMENT}from'./constants';/* SIMPLE API */exportconstincrement=()=>action(INCREMENT);exportconstadd=(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#constantsimport{createAction}from'typesafe-actions';import{Todo}from'../todos/models';exportconstemptyAction=createAction(INCREMENT)<void>();exportconstpayloadAction=createAction(ADD)<number>();exportconstpayloadMetaAction=createAction(ADD)<number,string>();exportconstpayloadCreatorAction=createAction('TOGGLE_TODO',(todo:Todo)=>todo.id)<string>();
Click to expand

import{store}from'../../store/';import{countersActionsascounter}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

⇧ back to top


Reducers

State with Type-level Immutability

Declare reducerState type withreadonly modifier to get compile time immutability

exporttypeState={readonlycounter:number;readonlytodos:ReadonlyArray<string>;};

Readonly modifier allow initialization, but will not allow reassignment by highlighting compiler errors

exportconstinitialState:State={counter:0,};// OKinitialState.counter=3;// TS Error: cannot be mutated

It's great forArrays in JS because it will error when using mutator methods like (push,pop,splice, ...), but it'll still allow immutable methods like (concat,map,slice,...).

state.todos.push('Learn about tagged union types')// TS Error: Property 'push' does not exist on type 'ReadonlyArray<string>'constnewTodos=state.todos.concat('Learn about tagged union types')// OK

Caveat -Readonly is not recursive

This means that thereadonly modifier doesn't propagate immutability down the nested structure of objects. You'll need to mark each property on each level explicitly.

TIP: useReadonly orReadonlyArrayMapped types

exporttypeState=Readonly<{counterPairs:ReadonlyArray<Readonly<{immutableCounter1:number,immutableCounter2:number,}>>,}>;state.counterPairs[0]={immutableCounter1:1,immutableCounter2:1};// TS Error: cannot be mutatedstate.counterPairs[0].immutableCounter1=1;// TS Error: cannot be mutatedstate.counterPairs[0].immutableCounter2=1;// TS Error: cannot be mutated

Solution - recursiveReadonly is calledDeepReadonly

To fix this we can useDeepReadonly type (available fromutility-types).

import{DeepReadonly}from'utility-types';exporttypeState=DeepReadonly<{containerObject:{innerValue:number,numbers:number[],}}>;state.containerObject={innerValue:1};// TS Error: cannot be mutatedstate.containerObject.innerValue=1;// TS Error: cannot be mutatedstate.containerObject.numbers.push(1);// TS Error: cannot use mutator methods

⇧ back to top

Typing reducer

to understand following section make sure to learn aboutType Inference,Control flow analysis andTagged union types

import{combineReducers}from'redux';import{ActionType}from'typesafe-actions';import{Todo,TodosFilter}from'./models';import*asactionsfrom'./actions';import{ADD,CHANGE_FILTER,TOGGLE}from'./constants';exporttypeTodosAction=ActionType<typeofactions>;exporttypeTodosState=Readonly<{todos:Todo[];todosFilter:TodosFilter;}>;constinitialState:TodosState={todos:[],todosFilter:TodosFilter.All,};exportdefaultcombineReducers<TodosState,TodosAction>({todos:(state=initialState.todos,action)=>{switch(action.type){caseADD:return[...state,action.payload];caseTOGGLE:returnstate.map(item=>item.id===action.payload            ?{ ...item,completed:!item.completed}            :item);default:returnstate;}},todosFilter:(state=initialState.todosFilter,action)=>{switch(action.type){caseCHANGE_FILTER:returnaction.payload;default:returnstate;}},});

⇧ back to top

Typing reducer withtypesafe-actions

Notice we are not required to use any generic type parameter in the API. Try to compare it with regular reducer as they are equivalent.

import{combineReducers}from'redux';import{createReducer}from'typesafe-actions';import{Todo,TodosFilter}from'./models';import{ADD,CHANGE_FILTER,TOGGLE}from'./constants';exporttypeTodosState=Readonly<{todos:Todo[];todosFilter:TodosFilter;}>;constinitialState:TodosState={todos:[],todosFilter:TodosFilter.All,};consttodos=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));consttodosFilter=createReducer(initialState.todosFilter).handleType(CHANGE_FILTER,(state,action)=>action.payload);exportdefaultcombineReducers({  todos,  todosFilter,});

⇧ back to top

Testing reducer

import{todosReducerasreducer,todosActionsasactions,}from'./';import{TodosState}from'./reducer';/** * FIXTURES */constgetInitialState=(initial?:Partial<TodosState>)=>reducer(initialasTodosState,{}asany);/** * STORIES */describe('Todos Stories',()=>{describe('initial state',()=>{it('should match a snapshot',()=>{constinitialState=getInitialState();expect(initialState).toMatchSnapshot();});});describe('adding todos',()=>{it('should add a new todo as the first element',()=>{constinitialState=getInitialState();expect(initialState.todos).toHaveLength(0);conststate=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',()=>{constactiveTodo={id:'1',completed:false,title:'active todo'};constinitialState=getInitialState({todos:[activeTodo]});expect(initialState.todos[0].completed).toBeFalsy();conststate1=reducer(initialState,actions.toggle(activeTodo.id));expect(state1.todos[0].completed).toBeTruthy();});});});

⇧ back to top


Async Flow withredux-observable

Typing epics

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!!!exportconstlogAddAction: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());

⇧ back to top

Testing epics

import{StateObservable,ActionsObservable}from'redux-observable';import{RootState,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 fileconstservices={logger:{log:jest.fn(),},localStorage:{loadState:jest.fn(),saveState:jest.fn(),},};describe('Todos Epics',()=>{letstate$:StateObservable<RootState>;beforeEach(()=>{state$=newStateObservable<RootState>(newSubject<RootState>(),undefinedasany);});describe('logging todos actions',()=>{beforeEach(()=>{services.logger.log.mockClear();});it('should call the logger service when adding a new todo',done=>{constaddTodoAction=add('new todo');constaction$=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 epicexpect(outputAction).toEqual(undefined);done();});});});});

⇧ back to top


Selectors withreselect

import{createSelector}from'reselect';import{TodosState}from'./reducer';exportconstgetTodos=(state:TodosState)=>state.todos;exportconstgetTodosFilter=(state:TodosState)=>state.todosFilter;exportconstgetFilteredTodos=createSelector(getTodos,getTodosFilter,(todos,todosFilter)=>{switch(todosFilter){case'completed':returntodos.filter(t=>t.completed);case'active':returntodos.filter(t=>!t.completed);default:returntodos;}});

⇧ back to top


Connect withreact-redux

Typing connected component

NOTE: Below you'll find a short explanation of concepts behind usingconnect with TypeScript. For more detailed examples please checkRedux Connected Components section.

importMyTypesfrom'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` argumentconstmapStateToProps=(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 functionconstmapDispatchToProps=(dispatch:Dispatch<MyTypes.RootAction>)=>bindActionCreators({onIncrement:countersActions.increment,},dispatch);// shorter alternative is to use an object instead of mapDispatchToProps functionconstdispatchToProps={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 approachexportconstFCCounterConnected=connect(mapStateToProps,mapDispatchToProps)(FCCounter);// You can add extra layer of validation of your action creators// by using bindActionCreators generic type parameter and RootAction typeconstmapDispatchToProps=(dispatch:Dispatch<MyTypes.RootAction>)=>bindActionCreators<ActionCreatorsMapObject<Types.RootAction>>({invalidActionCreator:()=>1,// Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... }},dispatch);

⇧ back to top

TypinguseSelector anduseDispatch

import{Dispatch}from'redux';import{TypedUseSelectorHook,useSelectorasuseGenericSelector,useDispatchasuseGenericDispatch}from'react-redux';import{RootState,RootAction}from'MyTypes';exportconstuseSelector:TypedUseSelectorHook<RootState>=useGenericSelector;exportconstuseDispatch:()=>Dispatch<RootAction>=useGenericDispatch;

⇧ back to top

Typing connected component withredux-thunk integration

NOTE: When using thunk action creators you need to usebindActionCreators. Only this way you can get corrected dispatch props type signature like below.*

WARNING: As of now (Apr 2019)bindActionCreators signature of the latestredux-thunk release will not work as below, you need to use our modified type definitions that you can find here/playground/typings/redux-thunk/index.d.ts and then addpaths overload in your tsconfig like this:"paths":{"redux-thunk":["typings/redux-thunk"]}.

constthunkAsyncAction=()=>async(dispatch:Dispatch):Promise<void>=>{// dispatch actions, return Promise, etc.}constmapDispatchToProps=(dispatch:Dispatch<Types.RootAction>)=>bindActionCreators({      thunkAsyncAction,},dispatch);typeDispatchProps=ReturnType<typeofmapDispatchToProps>;// { thunkAsyncAction: () => Promise<void>; }/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }

⇧ back to top


Configuration & Dev Tools

Common Npm Scripts

Common TS-related npm scripts shared across projects

"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":"eslint ./src --ext .js,.jsx,.ts,.tsx","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",

⇧ back to top

tsconfig.json

We have recommendedtsconfig.json that you can easily add to your project thanks toreact-redux-typescript-scripts package.

Click to expand

{"compilerOptions":{"target":"ES6","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"strict":true,"forceConsistentCasingInFileNames":true,"noFallthroughCasesInSwitch":true,"module":"esnext","moduleResolution":"node","resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"react-jsx"},"include":["src","typings"]}

⇧ back to top

TSLib

This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file.

https://www.npmjs.com/package/tslib

Installation
npm i tslib

Then add this to yourtsconfig.json:

"compilerOptions":{"importHelpers":true}

⇧ back to top

ESLint

We have recommended config that will automatically add a parser & plugin for TypeScript thanks toreact-redux-typescript-scripts package.

https://typescript-eslint.io

Installationnpm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

.eslintrc.js

Click to expand

module.exports={root:true,parser:'@typescript-eslint/parser',plugins:['@typescript-eslint'],extends:['react-app','react-app/jest','prettier'],rules:{'import/no-anonymous-default-export':0},};

⇧ back to top

Jest

https://jestjs.io/

Installation
npm i -D jest ts-jest @types/jest

jest.config.json

Click to expand

{"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/"}

jest.stubs.js

Click to expand

// Global/Window object Stubs for Jestwindow.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=()=>[];

⇧ back to top

Style Guides

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ back to top


FAQ

Ambient Modules

Imports in ambient modules

For type augmentation imports should stay outside of module declaration.

import{Operator}from'rxjs/Operator';import{Observable}from'rxjs/Observable';declare module'rxjs/Subject'{interfaceSubject<T>{lift<R>(operator:Operator<T,R>):Observable<R>;}}

When creating 3rd party type-definitions all the imports should be kept inside the module declaration, otherwise it will be treated as augmentation and show error

declare module"react-custom-scrollbars"{import*asReactfrom"react";exportinterfacepositionValues{    ...

⇧ back to top

Type-Definitions

Missing type-definitions error

if you cannot find types for a third-party module you can provide your own types or disable type-checking for this module usingShorthand Ambient Modules

// typings/modules.d.tsdeclare module'MyTypes';declare module'react-test-renderer';declare module'@storybook/addon-storyshots'

Using customd.ts files for npm modules

If you want to use an alternative (customized) type-definitions for some npm module (that usually comes with it's own type-definitions), you can do it by adding an override inpaths compiler option.

{"compilerOptions":{"baseUrl":".","paths":{"redux":["typings/redux"],// use an alternative type-definitions instead of the included one      ...},    ...,}}

⇧ back to top

Type Augmentation

Strategies to fix issues coming from external type-definitions files (*.d.ts)

Augmenting library internal declarations - using relative import

// added missing autoFocus Prop on Input component in "antd@2.10.0" npm packagedeclare module'../node_modules/antd/lib/input/Input'{exportinterfaceInputProps{autoFocus?:boolean;}}

Augmenting library public declarations - using node_modules import

// fixed broken public type-definitions in "rxjs@5.4.1" npm packageimport{Operator}from'rxjs/Operator';import{Observable}from'rxjs/Observable';declare module'rxjs/Subject'{interfaceSubject<T>{lift<R>(operator:Operator<T,R>):Observable<R>;}}

More advanced scenarios for working with vendor type-definitions can be found hereOfficial TypeScript Docs

⇧ back to top

Misc

- should I still use React.PropTypes in TS?

No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring Props and State interfaces, you will get complete intellisense and design-time safety with static type checking. This way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standardized method of documenting your component public API in the source code.

⇧ back to top

- when to useinterface declarations and whentype aliases?

From practical side, usinginterface declaration will create an identity (interface name) in compiler errors, on the contrarytype aliases doesn't create an identity and will be unwinded to show all the properties and nested types it consists of.
Although I prefer to usetype most of the time there are some places this can become too noisy when reading compiler errors and that's why I like to leverage this distinction to hide some of not so important type details in errors using interfaces identity.Relatedts-lint rule:https://palantir.github.io/tslint/rules/interface-over-type-literal/

⇧ back to top

- what's better default or named exports?

A common flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
With this solution you'll achieve better encapsulation and be able to safely refactor internal naming and folders structure without breaking your consumer code:

// 1. create your component files (`select.tsx`) using default export in some folder:// components/select.tsxconstSelect:React.FC<Props>=(props)=>{...exportdefaultSelect;// 2. in this folder create an `index.ts` file that will re-export components with named exports:// components/index.tsexport{ defaultasSelect}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.tsximport{ Select}from'@src/components';orimportSelectfrom'@src/components/select';...

⇧ back to top

- how to best initialize class instance or static properties?

Prefered modern syntax is to use class Property Initializers

classClassCounterWithInitialCountextendsReact.Component<Props,State>{// default props using Property InitializersstaticdefaultProps:DefaultProps={className:'default-class',initialCount:0,};// initial state using Property Initializersstate:State={count:this.props.initialCount,};  ...}

⇧ back to top

- how to best declare component handler functions?

Prefered modern syntax is to use Class Fields with arrow functions

classClassCounterextendsReact.Component<Props,State>{// handlers using Class Fields with arrow functionshandleIncrement=()=>{this.setState({count:this.state.count+1});};  ...}

⇧ back to top


Tutorials & Articles

Curated list of relevant in-depth tutorials

Higher-Order Components:

⇧ back to top


Contributors

Thanks goes to these wonderful people (emoji key):


Piotrek Witek

💻📖🤔👀💬

Kazz Yokomizo

💵🔍

Jake Boone

📖

Amit Dahan

📖

gulderov

📖

Erik Pearson

📖

Bryan Mason

📖

Jakub Chodorowicz

💻

Oleg Maslov

🐛

Aaron Westbrook

🐛

Peter Blazejewicz

📖

Solomon White

📖

Levi Rocha

📖

Sudachi-kun

💵

Sosuke Suzuki

💻

Tom Rathbone

📖

Arshad Kazmi

📖

JeongUkJae

📖

This project follows theall-contributors specification. Contributions of any kind welcome!


MIT License

Copyright (c) 2017 Piotr Witekpiotrek.witek@gmail.com (https://piotrwitek.github.io)

About

The complete guide to static typing in "React & Redux" apps using TypeScript

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors29


[8]ページ先頭

©2009-2025 Movatter.jp