- Notifications
You must be signed in to change notification settings - Fork0
[Deconstruct] (Move to Playground) React Redux Examples, Fork ofhttps://github.com/piotrwitek/react-redux-typescript-guide
License
GrayStrider/react-redux-typescript-guide
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
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."
⭐Found it useful? Want more updates?Show your support by giving a ⭐
🎉Now updated to supportTypeScript v3.4 🎉


Goals
- Complete type safety (with
--strictflag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking withanytype) - 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
Codesandbox links
- Reference Todo-App implementation usingReact, Redux, Typescript Guide:Link
Playground Project
You should check out Playground Project located in the/playground folder. It is a source of all 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.
We are open for contributions. If you're planning to contribute please make sure to read the contributing guide:CONTRIBUTING.md
This is an independent open-source project created by people investing their free time for the benefit of our community.
If you are using it please consider donating as this will guarantee the project will be updated and maintained in the long run.
Issues can be funded by anyone interested in them being resolved. Reward will be transparently distributed to the contributor handling the task through the IssueHunt platform.
🌟 -New or updated section
- Installation
- React - Type-Definitions Cheatsheet
- React - Typing Patterns
- Redux - Typing Patterns
- Configuration & Dev Tools
- Recipes
- Tutorials & Articles
- Contributors
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. To make it work with Redux v3.x.x please refer tothis config)
Type representing a functional component
constMyComponent:React.FC<Props>= ...
Type representing a class component
classMyComponentextendsReact.Component<Props,State>{ ...
Gets type of Component Props, so you don't need to export Props from your component ever! (Works for both FC and Class components)
typeMyComponentProps=React.ComponentProps<typeofMyComponent>;
Type representing union type of (React.FC | React.Component)
constwithState=<PextendsWrappedComponentProps>(WrappedComponent:React.ComponentType<P>,)=>{ ...
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/>;
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})=> ...
Type representing style object in JSX (useful for css-in-js styles)
conststyles:React.CSSProperties={flexDirection:'row', ...constelement=<divstyle={styles}...
Type representing generic event handler
consthandleChange:React.ReactEventHandler<HTMLInputElement>=(ev)=>{ ...}<inputonChange={handleChange}.../>
Type representing more specific event handler
consthandleChange=(ev:React.MouseEvent<HTMLDivElement>)=>{ ...}<divonMouseMove={handleChange}.../>
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>);};
- spread attributeslink
import*asReactfrom'react';typeProps={className?:string;style?:React.CSSProperties;};exportconstFCSpreadAttributes:React.FC<Props>=props=>{const{ children, ...restProps}=props;return<div{...restProps}>{children}</div>;};
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>);}}
import*asReactfrom'react';typeProps={label:string;initialCount:number;};typeState={count:number;};exportclassClassCounterWithDefaultPropsextendsReact.Component<Props,State>{staticdefaultProps={initialCount:0,};readonlystate:State={count:this.props.initialCount,};componentWillReceiveProps({ initialCount}:Props){if(initialCount!=null&&initialCount!==this.props.initialCount){this.setState({count: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>);}}
- easily create typed component variations and reuse common logic
- common use case is a generic list components
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>);}}
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);}}
Mousecomponent 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>);}}
Adds state to a stateless counter
import*asReactfrom'react';import{Subtract}from'utility-types';// These props will be subtracted from base component propsinterfaceInjectedProps{count:number;onIncrement:()=>void;}exportconstwithState=<BasePropsextendsInjectedProps>(_BaseComponent:React.ComponentType<BaseProps>)=>{// fix for TypeScript issues: https://github.com/piotrwitek/react-redux-typescript-guide/issues/111constBaseComponent=_BaseComponentasReact.ComponentType<InjectedProps>;typeHocProps=Subtract<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(<BaseComponentcount={count}// injectedonIncrement={this.handleIncrement}// injected{...restProps}/>);}};};
Click to expand
import*asReactfrom'react';import{withState}from'../hoc';import{FCCounter}from'../components';constFCCounterWithState=withState(FCCounter);exportdefault()=><FCCounterWithStatelabel={'FCCounterWithState'}/>;
Adds error handling using componentDidCatch to any component
import*asReactfrom'react';import{Subtract}from'utility-types';constMISSING_ERROR='Error was swallowed during propagation.';// These props will be subtracted from base component propsinterfaceInjectedProps{onReset:()=>void;}exportconstwithErrorBoundary=<BasePropsextendsInjectedProps>(_BaseComponent:React.ComponentType<BaseProps>)=>{// fix for TypeScript issues: https://github.com/piotrwitek/react-redux-typescript-guide/issues/111constBaseComponent=_BaseComponentasReact.ComponentType<InjectedProps>;typeHocProps=Subtract<BaseProps,InjectedProps>&{// 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};handleReset=()=>{this.setState({error:undefined});};render(){const{ children, ...restProps}=this.props;const{ error}=this.state;if(error){return(<BaseComponentonReset={this.handleReset}// injected{...restProps}/>);}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>);
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'}/>;
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}/>);
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'}/>);
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;
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>);}}
import*asReactfrom'react';importThemeContextfrom'./theme-context';typeProps={};exportdefaultfunctionToggleThemeButton(props:Props){return(<ThemeContext.Consumer>{({ theme, toggleTheme})=><buttonstyle={theme}onClick={toggleTheme}{...props}/>}</ThemeContext.Consumer>);}
import*asReactfrom'react';importThemeContextfrom'./theme-context';typeProps={};exportclassToggleThemeButtonClassextendsReact.Component<Props>{staticcontextType=ThemeContext;context!:React.ContextType<typeofThemeContext>;render(){const{ theme, toggleTheme}=this.context;return(<buttonstyle={theme}onClick={toggleTheme}> Toggle Theme</button>);}}
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></>);}
Hook for state management like Redux in a function component.
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;
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>);}
Can be imported in connected components to provide type-safety to Reduxconnect function
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('./index').default>;exporttypeRootAction=ActionType<typeofimport('./root-action').default>;exporttypeRootState=StateType<typeofimport('./root-reducer').default>;}declare module'typesafe-actions'{interfaceTypes{RootAction:ActionType<typeofimport('./root-action').default>;}}
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 like
getStateordispatchwill be type checked and will expose all type errors
import{RootAction,RootState,Services}from'MyTypes';import{createStore,applyMiddleware}from'redux';import{createEpicMiddleware}from'redux-observable';import{composeEnhancers}from'./utils';importrootReducerfrom'./root-reducer';importrootEpicfrom'./root-epic';importservicesfrom'../services';exportconstepicMiddleware=createEpicMiddleware<RootAction,RootAction,RootState,Services>({dependencies:services,});// configure middlewaresconstmiddlewares=[epicMiddleware];// compose enhancersconstenhancer=composeEnhancers(applyMiddleware(...middlewares));// rehydrate state on app startconstinitialState={};// create storeconststore=createStore(rootReducer,initialState,enhancer);epicMiddleware.run(rootEpic);// export store singleton instanceexportdefaultstore;
We'll be using a battle-tested library
that'll help retain complete type soundness and simplify maintenace oftypes in Redux Architectures
typesafe-actions
You can find more real-world examples and in-depth tutorial in: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.
import{action}from'typesafe-actions';import{ADD,INCREMENT}from'./constants';// CLASSIC APIexportconstincrement=()=>action(INCREMENT);exportconstadd=(amount:number)=>action(ADD,amount);// ALTERNATIVE API - allow to use reference to "action-creator" function instead of "type constant"// e.g. case getType(increment): return { ... }// This will allow to completely eliminate need for "constants" in your application, more info here:// https://github.com/piotrwitek/typesafe-actions#behold-the-mighty-tutorial// OPTION 1 (with generics):// import { createStandardAction } from 'typesafe-actions';// export const increment = createStandardAction(INCREMENT)<void>();// export const add = createStandardAction(ADD)<number>();// OPTION 2 (with resolve callback):// import { createAction } from 'typesafe-actions';// export const increment = createAction(INCREMENT);// export const add = createAction(ADD, resolve => {// return (amount: number) => resolve(amount);// });
Click to expand
importstorefrom'../../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
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
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: use
ReadonlyorReadonlyArrayMapped 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
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
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;}},});
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).handleAction(ADD,(state,action)=>[...state,action.payload]).handleAction(TOGGLE,(state,action)=>state.map(item=>item.id===action.payload ?{ ...item,completed:!item.completed} :item));consttodosFilter=createReducer(initialState.todosFilter).handleAction(CHANGE_FILTER,(state,action)=>action.payload);exportdefaultcombineReducers({ todos, todosFilter,});
import{todosReducerasreducer,todosActionsasactions,TodosState,}from'./';/** * 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();});});});
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());
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 fileconstservices={logger:{log:jest.fn<Services['logger']['log']>(),},localStorage:{loadState:jest.fn<Services['localStorage']['loadState']>(),saveState:jest.fn<Services['localStorage']['saveState']>(),},};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();});});});});
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;}});
NOTE: Below you'll find only a short explanation of concepts behind typingconnect. For more real-world 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);
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 updated 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>; }
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": "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",We have our own recommendedtsconfig.json that you can easily add to your project thanks toreact-redux-typescript-scripts package.
Click to expand
{"include":["src","typings"],"exclude":["src/**/*.spec.*"],"extends":"./node_modules/react-redux-typescript-scripts/tsconfig.json","compilerOptions":{}}
https://www.npmjs.com/package/tslib
This library will cut down on your bundle size, thanks to using external runtime helpers instead of adding them per each file.
Installation
npm i tslib
Then add this to yourtsconfig.json:
"compilerOptions":{"importHelpers":true}
https://palantir.github.io/tslint/
Installation
npm i -D tslint
For React project you should add additional
reactspecific rules:npm i -D tslint-reacthttps://github.com/palantir/tslint-react
We have our own recommended config that you can easily add to your project thanks toreact-redux-typescript-scripts package.
Click to expand
{"extends":["react-redux-typescript-scripts/tslint.json","react-redux-typescript-scripts/tslint-react.json"],"rules":{// you can further customize options here}}
https://eslint.org/
https://typescript-eslint.io
Installation
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
We have our own recommended config that will automatically add a parser & plugin for TypeScript thanks toreact-redux-typescript-scripts package.
Click to expand
{"extends":["react-app","./node_modules/react-redux-typescript-scripts/eslint.js"],"rules":{// you can further customize options here}}
Installation
npm i -D jest ts-jest @types/jest
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/"}
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=()=>[];
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.
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/
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';...
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,}; ...}
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});}; ...}
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 decleration, otherwise it will be treated as augmentation and show error
declare module"react-custom-scrollbars"{import*asReactfrom"react";exportinterfacepositionValues{ ...
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';
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 ...}, ...,}}
Strategies to fix issues coming from external type-definitions files (*.d.ts)
// added missing autoFocus Prop on Input component in "antd@2.10.0" npm packagedeclare module'../node_modules/antd/lib/input/Input'{exportinterfaceInputProps{autoFocus?:boolean;}}
// 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
Curated list of relevant in-depth tutorials
Higher-Order Components:
Thanks goes to these wonderful people (emoji key):
This project follows theall-contributors specification. Contributions of any kind welcome!
MIT License
Copyright (c) 2017 Piotr Witekpiotrek.witek@gmail.com (http://piotrwitek.github.io)
About
[Deconstruct] (Move to Playground) React Redux Examples, Fork ofhttps://github.com/piotrwitek/react-redux-typescript-guide
Topics
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.