- Notifications
You must be signed in to change notification settings - Fork0
The complete guide to static typing in "React & Redux" apps using TypeScript
License
sneakyweasel/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 be compatible withTypeScript v3.1.6 🎉
💻Reference implementation of Todo-App withtypesafe-actions:https://codesandbox.io/s/github/piotrwitek/typesafe-actions-todo-app 💻
- 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
- utility-types - Collection of generic types for TypeScript, complementing built-in mapped types and aliases - think lodash for reusable types.
- typesafe-actions - Typesafe utilities for "action-creators" in Redux / Flux Architecture
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.
Playground was created in such a way that you can simply clone the repository locally and immediately play around on your own. It will help you to learn all the examples from this guide in a real project environment without the need to create some 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 and the money will be transparently distributed to the contributors handling a particular issue.
- Introduction
- React - Type-Definitions Cheatsheet
- React - Typing Patterns
- Redux - Typing Patterns
- Tools
- Recipes
- FAQ
- Tutorials
- 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 (usefull 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:()=>any;};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 original component typeinterfaceInjectedProps{count:number;onIncrement:()=>any;}exportconstwithState=<WrappedPropsextendsInjectedProps>(WrappedComponent:React.ComponentType<WrappedProps>)=>{// These props will be added to original component typetypeHocProps=Subtract<WrappedProps,InjectedProps>&{// here you can extend hoc propsinitialCount?:number;};typeHocState={readonlycount:number;};returnclassWithStateextendsReact.Component<HocProps,HocState>{// Enhance component name for debugging and React-Dev-ToolsstaticdisplayName=`withState(${WrappedComponent.name})`;// reference to original wrapped componentstaticreadonlyWrappedComponent=WrappedComponent;readonlystate:HocState={count:Number(this.props.initialCount)||0,};handleIncrement=()=>{this.setState({count:this.state.count+1});};render(){const{ ...restProps}=this.propsas{};const{ count}=this.state;return(<WrappedComponent{...restProps}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'}/>;
Adds error handling using componentDidCatch to any component
import*asReactfrom'react';import{Subtract}from'utility-types';constMISSING_ERROR='Error was swallowed during propagation.';interfaceInjectedProps{onReset:()=>any;}exportconstwithErrorBoundary=<WrappedPropsextendsInjectedProps>(WrappedComponent:React.ComponentType<WrappedProps>)=>{typeHocProps=Subtract<WrappedProps,InjectedProps>&{// here you can extend hoc props};typeHocState={readonlyerror:Error|null|undefined;};returnclassWithErrorBoundaryextendsReact.Component<HocProps,HocState>{staticdisplayName=`withErrorBoundary(${WrappedComponent.name})`;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 cloud};handleReset=()=>{this.setState({error:undefined});};render(){const{ children, ...restProps}=this.propsas{children:React.ReactNode;};const{ error}=this.state;if(error){return(<WrappedComponent{...restProps}onReset={this.handleReset}// injected/>);}returnchildren;}};};
Click to expand
import*asReactfrom'react';import{withErrorBoundary}from'../hoc';import{ErrorMessage}from'../components';constErrorMessageWithErrorBoundary=withErrorBoundary(ErrorMessage);constBrokenButton=()=>(<buttontype="button"onClick={()=>{thrownewError(`Catch me!`);}}>{`Throw nasty error`}</button>);exportdefault()=>(<ErrorMessageWithErrorBoundary><BrokenButton/></ErrorMessageWithErrorBoundary>);
If you try to useconnect orbindActionCreators explicitly and want to type your component callback props as() => void this will raise compiler errors. It happens becausebindActionCreators typings will not map the return type of action creators tovoid, due to a current TypeScript limitations.
A decent alternative I can recommend is to use() => any type, it will work just fine in all possible scenarios and should not cause any typing problems whatsoever. All the code examples in the Guide withconnect are also using this pattern.
If there is any progress or fix in regard to the above caveat I'll update the guide and make an announcement on my twitter/medium (There are a few existing proposals already).
There is alternative way to retain type soundness but it requires an explicit wrapping with
dispatchand will be very tedious for the long run. See example below:
constmapDispatchToProps=(dispatch:Dispatch<ActionType>)=>({onIncrement:()=>dispatch(actions.increment()),});
importTypesfrom'Types';import{connect}from'react-redux';import{countersActions,countersSelectors}from'../features/counters';import{FCCounter}from'../components';constmapStateToProps=(state:Types.RootState)=>({count:countersSelectors.getReduxCounter(state.counters),});exportconstFCCounterConnected=connect(mapStateToProps,{onIncrement:countersActions.increment,})(FCCounter);
Click to expand
import*asReactfrom'react';import{FCCounterConnected}from'.';exportdefault()=><FCCounterConnectedlabel={'FCCounterConnected'}/>;
importTypesfrom'Types';import{bindActionCreators,Dispatch}from'redux';import{connect}from'react-redux';import{countersActions}from'../features/counters';import{FCCounter}from'../components';constmapStateToProps=(state:Types.RootState)=>({count:state.counters.reduxCounter,});constmapDispatchToProps=(dispatch:Dispatch<Types.RootAction>)=>bindActionCreators({onIncrement:countersActions.increment,},dispatch);exportconstFCCounterConnectedVerbose=connect(mapStateToProps,mapDispatchToProps)(FCCounter);
Click to expand
import*asReactfrom'react';import{FCCounterConnectedVerbose}from'.';exportdefault()=>(<FCCounterConnectedVerboselabel={'FCCounterConnectedVerbose'}/>);
importTypesfrom'Types';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),});exportconstFCCounterConnectedExtended=connect(mapStateToProps,{onIncrement:countersActions.increment,})(FCCounter);
Click to expand
import*asReactfrom'react';import{FCCounterConnectedExtended}from'.';exportdefault()=>(<FCCounterConnectedExtendedlabel={'FCCounterConnectedExtended'}initialCount={10}/>);
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';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'};constinitialState:State={count:0,};functionreducer(state:State,action:Action):State{switch(action.type){case'reset':returninitialState;case'increment':return{count:state.count+1};case'decrement':return{count:state.count-1};default:returnstate;}}interfaceCounterProps{initialCount:number;}functionCounter({ initialCount}:CounterProps){const[state,dispatch]=React.useReducer<State,Action>(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>);}
We'll be using a battle-tested library
that automates and simplify maintenace oftype annotations in Redux Architectures
typesafe-actions
You should readThe Mighty Tutorial to learn it all the easy way!
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.
To fix this we can useDeepReadonly type (available inutility-types npm library - collection of reusable types extending the collection ofstandard-lib in TypeScript.
Check the example below:
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
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 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';exporttypeTodosState={readonlytodos:Todo[];readonlytodosFilter:TodosFilter;};exporttypeTodosAction=ActionType<typeofactions>;exportdefaultcombineReducers<TodosState,TodosAction>({todos:(state=[],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=TodosFilter.All,action)=>{switch(action.type){caseCHANGE_FILTER:returnaction.payload;default:returnstate;}},});
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();});});});
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}from'typesafe-actions';import{RouterAction,LocationChangeAction}from'react-router-redux';typeReactRouterAction=RouterAction|LocationChangeAction;import{CountersAction}from'../features/counters';importrootReducerfrom'./root-reducer';declare module'Types'{exporttypeRootState=StateType<typeofrootReducer>;exporttypeRootAction=ReactRouterAction|CountersAction;}
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{createStore,applyMiddleware}from'redux';import{createEpicMiddleware}from'redux-observable';import{composeEnhancers}from'./utils';importrootReducerfrom'./root-reducer';importrootEpicfrom'./root-epic';importservicesfrom'../services';exportconstepicMiddleware=createEpicMiddleware(rootEpic,{dependencies:services,});functionconfigureStore(initialState?:object){// configure middlewaresconstmiddlewares=[epicMiddleware];// compose enhancersconstenhancer=composeEnhancers(applyMiddleware(...middlewares));// create storereturncreateStore(rootReducer,initialState!,enhancer);}// pass an optional param to rehydrate state on app startconststore=configureStore();// export store singleton instanceexportdefaultstore;
For more examples and in-depth explanation you should readThe Mighty Tutorial to learn it all the easy way!
importTypesfrom'Types';import{combineEpics,Epic}from'redux-observable';import{tap,ignoreElements,filter}from'rxjs/operators';import{isOfType}from'typesafe-actions';import{todosConstants,TodosAction}from'../todos';// contrived example!!!constlogAddAction:Epic<TodosAction,Types.RootState,Types.Services>=(action$,store,{ 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());exportdefaultcombineEpics(logAddAction);
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;}});
Below snippet can be find in theplayground/ folder, you can checkout the repo and follow all dependencies to understand the bigger picture.playground/src/connected/fc-counter-connected-verbose.tsx
importTypesfrom'Types';import{bindActionCreators,Dispatch}from'redux';import{connect}from'react-redux';import{countersActions}from'../features/counters';import{FCCounter}from'../components';// `state` parameter needs a type annotation to type-check the correct shape of a state object but also it'll be used by "type inference" to infer the type of returned propsconstmapStateToProps=(state:Types.RootState,ownProps:FCCounterProps)=>({count:state.counters.reduxCounter,});// `dispatch` parameter needs a type annotation to type-check the correct shape of an action object when using dispatch functionconstmapDispatchToProps=(dispatch:Dispatch<Types.RootAction>)=>bindActionCreators({onIncrement:countersActions.increment,// without using action creators, this will be validated using your RootAction union type// onIncrement: () => dispatch({ type: "counters/INCREMENT" }),},dispatch);// NOTE: We don't need to pass generic type arguments to neither connect nor mapping functions because type inference will do all this work automatically. So there's really no reason to increase the noise ratio in your codebase!exportconstFCCounterConnectedVerbose=connect(mapStateToProps,mapDispatchToProps)(FCCounter);
Installation
npm i -D tslint
- Recommended setup is to extend build-in preset
tslint:recommended(usetslint:allto enable all rules) - Add additional
reactspecific rules:npm i -D tslint-reacthttps://github.com/palantir/tslint-react - Overwritten some defaults for more flexibility
Click to expand
{"extends":["tslint:recommended","tslint-react"],"rules":{"arrow-parens":false,"arrow-return-shorthand":[false],"comment-format":[true,"check-space"],"import-blacklist":[true],"interface-over-type-literal":false,"interface-name":false,"max-line-length":[true,120],"member-access":false,"member-ordering":[true,{"order":"fields-first"}],"newline-before-return":false,"no-any":false,"no-empty-interface":false,"no-import-side-effect":[true],"no-inferrable-types":[true,"ignore-params","ignore-properties"],"no-invalid-this":[true,"check-function-in-method"],"no-namespace":false,"no-null-keyword":false,"no-require-imports":false,"no-submodule-imports":[true,"@src","rxjs"],"no-this-assignment":[true,{"allow-destructuring":true}],"no-trailing-whitespace":true,"object-literal-sort-keys":false,"object-literal-shorthand":false,"one-variable-per-declaration":[false],"only-arrow-functions":[true,"allow-declarations"],"ordered-imports":[false],"prefer-method-signature":false,"prefer-template":[true,"allow-single-concat"],"quotemark":[true,"single","jsx-double"],"semicolon":[true,"always","ignore-bound-class-methods"],"trailing-comma":[true,{"singleline":"never","multiline":{"objects":"always","arrays":"always","functions":"ignore","typeLiterals":"ignore"},"esSpecCompliant":true}],"triple-equals":[true,"allow-null-check"],"type-literal-delimiter":true,"typedef":[true,"parameter","property-declaration"],"variable-name":[true,"ban-keywords","check-format","allow-pascal-case","allow-leading-underscore"],// tslint-react"jsx-no-multiline-js":false,"jsx-no-lambda":false}}
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=()=>[];
Common TS-related npm scripts shared across projects
"lint": "tslint -p ./","tsc": "tsc -p ./ --noEmit","tsc:watch": "tsc -p ./ --noEmit -w","pretest": "npm run lint & npm run tsc","test": "jest --config jest.config.json","test:watch": "jest --config jest.config.json --watch","test:update": "jest --config jest.config.json -u",- Recommended baseline config carefully optimized for strict type-checking and optimal webpack workflow
- Install
tslibto cut on bundle size, by using external runtime helpers instead of adding them inline:npm i tslib - Example "paths" setup for baseUrl relative imports with Webpack
Click to expand
{"compilerOptions":{"baseUrl":"./",// relative paths base"paths":{// "@src/*": ["src/*"] // will enable import aliases -> import { ... } from '@src/components'// WARNING: Require to add this to your webpack config -> resolve: { alias: { '@src': PATH_TO_SRC }}// "redux": ["typings/redux"], // override library types with your alternative type-definitions in typings folder},"outDir":"dist/",// target for compiled files"allowSyntheticDefaultImports":true,// no errors with commonjs modules interop"esModuleInterop":true,// enable to do "import React ..." instead of "import * as React ...""allowJs":true,// include js files"checkJs":true,// typecheck js files"declaration":false,// don't emit declarations"emitDecoratorMetadata":true,// include only if using decorators"experimentalDecorators":true,// include only if using decorators"forceConsistentCasingInFileNames":true,"importHelpers":true,// importing transpilation helpers from tslib"noEmitHelpers":true,// disable inline transpilation helpers in each file"jsx":"react",// transform JSX"lib":["dom","es2017"],// you will need to include polyfills for es2017 manually"types":["jest"],// which global types to use"target":"es5",// "es2015" for ES6+ engines"module":"es2015",// "es2015" for tree-shaking"moduleResolution":"node","noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noUnusedLocals":true,"strict":true,"pretty":true,"removeComments":true,"sourceMap":true},"include":["src","typings"]}
Most flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
Using this solution you'll achieve better encapsulation for internal structure/naming refactoring without breaking your consumer code:
// 1. in `components/` folder create component file (`select.tsx`) with default export:// components/select.tsxconstSelect:React.FC<Props>=(props)=>{...exportdefaultSelect;// 2. in `components/` folder create `index.ts` file handling named imports:// 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';...
When creating 3rd party modules declarations all the imports should be put inside the module decleration, otherwise it will be treated as augmentation and show error
declare module"react-custom-scrollbars"{import*asReactfrom"react";exportinterfacepositionValues{ ...
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 declaration 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>;}}
To quick-fix missing type-definitions for vendor modules you can "assert" a module type withany usingShorthand Ambient Modules
// typings/modules.d.tsdeclare module'Types';declare module'react-test-renderer';
More advanced scenarios for working with vendor type-definitions can be found hereOfficial TypeScript Docs
If you want to use an alternative (customized) type-definitions for some npm library (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 ...}, ...,}}
No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring IProps and IState interfaces, you will get complete intellisense and compile-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 external API in the source code.
From practical side, using
interfacedeclaration will display identity (interface name) in compiler errors, on the contrarytypealiases will be unwinded to show all the properties and nested types it consists of. This can be a bit noisy when reading compiler errors and I like to leverage this distinction to hide some of not so important type details in errors
Relatedts-lintrule:https://palantir.github.io/tslint/rules/interface-over-type-literal/
Prefered modern style 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 style 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});}; ...}
Curated list of relevant in-depth tutorials
Higher-Order Components:
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 💵 |
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
The complete guide to static typing in "React & Redux" apps using TypeScript
Resources
License
Contributing
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Languages
- TypeScript90.5%
- JavaScript8.4%
- Other1.1%