- Notifications
You must be signed in to change notification settings - Fork0
The complete guide to static typing in "React & Redux" apps using TypeScript
License
Srikanththyagarajan/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 it's Ecosystem) in afunctional style with TypeScript and to make your codecompletely type-safe while focusing on aconciseness of type annotations so it's a minimal effort to write and to maintain types in the long run."
To provide the best experience we focus on the symbiosis of type-safe complementary libraries and learning the concepts likeType Inference,Control flow analysis,Generics and someAdvanced Types.
(Compatible withTypeScript v2.7.2)
Found it usefull? Want more updates?Give it a 🌟
- Complete type safety (with
--strictflag) without loosing type information downstream through all the layers of our application (e.g. no type assertions or hacking withanytype) - Make type annotations concise by eliminating redudancy in types using advanced TypeScript Language features likeType Inference andControl flow analysis
- Reduce repetition and complexity of types with TypeScript focusedcomplementary libraries
You should check 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 is such a way, that you can simply clone the repository locally and immediately play around on your own to learn all the examples from this guide in a real project environment without the need to create some complicated environment setup by yourself.
- Type Definitions & Complementary Libraries
- React Types Cheatsheet 🌟NEW
- Component Typing Patterns
- Redux
- Action Creators 📝UPDATED
- Reducers 📝UPDATED
- Store Configuration 📝UPDATED
- Async Flow 📝UPDATED
- Selectors
- Tools
- Recipes
- FAQ
- Contribution Guide
- Project Examples
- Tutorials
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 from Redux v4.x.x (Beta). To make it work with Redux v3.x.x please refer tothis config)
Utility librarieswith focus on type-safety providing a light functional abstractions for common use-cases
- "utility-types" - Utility Types for TypeScript (think lodash for types, moreover provides migration from Flow's Utility Types)
- "typesafe-actions" - Typesafe Action Creators for Redux / Flux Architectures (in TypeScript)
Type representing stateless functional component
constMyComponent:React.SFC<MyComponentProps>= ...
Type representing statefull class component
classMyComponentextendsReact.Component<MyComponentProps,State>{ ...
Type representing union type of (SFC | Component)
constwithState=<PextendsWrappedComponentProps>(WrappedComponent:React.ComponentType<P>,)=>{ ...
Type representing a concept of React Element - representation of a native DOM component (
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';exportinterfaceSFCCounterProps{label:string;count:number;onIncrement:()=>any;}exportconstSFCCounter:React.SFC<SFCCounterProps>=(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';exportinterfaceSFCSpreadAttributesProps{className?:string;style?:React.CSSProperties;}exportconstSFCSpreadAttributes:React.SFC<SFCSpreadAttributesProps>=(props)=>{const{ children, ...restProps}=props;return(<div{...restProps}>{children}</div>);};
import*asReactfrom'react';exportinterfaceStatefulCounterProps{label:string;}typeState={count:number;};exportclassStatefulCounterextendsReact.Component<StatefulCounterProps,State>{state:State={count:0,};handleIncrement=()=>{this.setState({count:this.state.count+1});}render(){const{ handleIncrement}=this;const{ label}=this.props;const{ count}=this.state;return(<div><span>{label}:{count}</span><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);}}
import*asReactfrom'react';exportinterfaceStatefulCounterWithDefaultProps{label:string;initialCount?:number;}interfaceDefaultProps{initialCount:number;}interfaceState{count:number;}exportconstStatefulCounterWithDefault:React.ComponentClass<StatefulCounterWithDefaultProps>=classextendsReact.Component<StatefulCounterWithDefaultProps&DefaultProps>{// to make defaultProps strictly typed we need to explicitly declare their type//@see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11640staticdefaultProps:DefaultProps={initialCount:0,};state:State={count:this.props.initialCount,};componentWillReceiveProps({ initialCount}:StatefulCounterWithDefaultProps){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{name:string;}exportclassNameProviderextendsReact.Component<NameProviderProps,NameProviderState>{state={name:'Piotr',};render(){returnthis.props.children(this.state);}}
Mousecomponent found inRender Props React Docs
import*asReactfrom'react';exportinterfaceMouseProviderProps{render:(state:MouseProviderState)=>React.ReactNode;}interfaceMouseProviderState{x:number;y:number;}exportclassMouseProviderextendsReact.Component<MouseProviderProps,MouseProviderState>{state={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 typeinterfaceWrappedComponentProps{count:number;onIncrement:()=>any;}exportconstwithState=<PextendsWrappedComponentProps>(WrappedComponent:React.ComponentType<P>)=>{// These props will be added to original component typeinterfaceProps{initialCount?:number;}interfaceState{count:number;}returnclassWithStateextendsReact.Component<Subtract<P,WrappedComponentProps>&Props,State>{// Enhance component name for debugging and React-Dev-ToolsstaticdisplayName=`withState(${WrappedComponent.name})`;state:State={count:Number(this.props.initialCount)||0,};handleIncrement=()=>{this.setState({count:this.state.count+1});}render(){const{ ...remainingProps}=this.props;const{ count}=this.state;return(<WrappedComponent{...remainingProps}count={count}onIncrement={this.handleIncrement}/>);}};};
show usage
import*asReactfrom'react';import{withState}from'@src/hoc';import{SFCCounter}from'@src/components';constSFCCounterWithState=withState(SFCCounter);exportdefault(()=>(<SFCCounterWithStatelabel={'SFCCounterWithState'}/>))asReact.SFC<{}>;
Adds error handling using componentDidCatch to any component
import*asReactfrom'react';import{Subtract}from'utility-types';constMISSING_ERROR='Error was swallowed during propagation.';interfaceWrappedComponentProps{onReset?:()=>any;}exportconstwithErrorBoundary=<PextendsWrappedComponentProps>(WrappedComponent:React.ComponentType<P>)=>{interfaceProps{}interfaceState{error:Error|null|undefined;}returnclassWithErrorBoundaryextendsReact.Component<Subtract<P,WrappedComponentProps>&Props,State>{staticdisplayName=`withErrorBoundary(${WrappedComponent.name})`;state:State={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, ...remainingProps}=this.props;const{ error}=this.state;if(error){return(<WrappedComponent{...remainingProps}onReset={this.handleReset}/>);}returnchildren;}};};
show usage
import*asReactfrom'react';import{withErrorBoundary}from'@src/hoc';import{ErrorMessage}from'@src/components';constErrorMessageWithErrorBoundary=withErrorBoundary(ErrorMessage);constErrorThrower=()=>(<buttontype="button"onClick={()=>{thrownewError(`Catch this!`);}}>{`Throw nasty error`}</button>);exportdefault(()=>(<ErrorMessageWithErrorBoundary><ErrorThrower/></ErrorMessageWithErrorBoundary>))asReact.SFC<{}>;
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:
const mapDispatchToProps = (dispatch: Dispatch) => ({ onIncrement: () => dispatch(actions.increment()),});import{connect}from'react-redux';import{RootState}from'@src/redux';import{countersActions,countersSelectors}from'@src/redux/counters';import{SFCCounter}from'@src/components';constmapStateToProps=(state:RootState)=>({count:countersSelectors.getReduxCounter(state),});exportconstSFCCounterConnected=connect(mapStateToProps,{onIncrement:countersActions.increment,})(SFCCounter);
show usage
import*asReactfrom'react';import{SFCCounterConnected}from'@src/connected';exportdefault()=>(<SFCCounterConnectedlabel={'SFCCounterConnected'}/>);
import{bindActionCreators}from'redux';import{connect}from'react-redux';import{RootState,Dispatch}from'@src/redux';import{countersActions}from'@src/redux/counters';import{SFCCounter}from'@src/components';constmapStateToProps=(state:RootState)=>({count:state.counters.reduxCounter,});constmapDispatchToProps=(dispatch:Dispatch)=>bindActionCreators({onIncrement:countersActions.increment,},dispatch);exportconstSFCCounterConnectedVerbose=connect(mapStateToProps,mapDispatchToProps)(SFCCounter);
show usage
import*asReactfrom'react';import{SFCCounterConnectedVerbose}from'@src/connected';exportdefault()=>(<SFCCounterConnectedVerboselabel={'SFCCounterConnectedVerbose'}/>);
import{connect}from'react-redux';import{RootState}from'@src/redux';import{countersActions,countersSelectors}from'@src/redux/counters';import{SFCCounter}from'@src/components';exportinterfaceSFCCounterConnectedExtended{initialCount:number;}constmapStateToProps=(state:RootState,ownProps:SFCCounterConnectedExtended)=>({count:countersSelectors.getReduxCounter(state)+ownProps.initialCount,});exportconstSFCCounterConnectedExtended=connect(mapStateToProps,{onIncrement:countersActions.increment,})(SFCCounter);
show usage
import*asReactfrom'react';import{SFCCounterConnectedExtended}from'@src/connected';exportdefault()=>(<SFCCounterConnectedExtendedlabel={'SFCCounterConnectedExtended'}initialCount={10}/>);
Using Typesafe Action Creators helpers for Redux
typesafe-actions
A recommended approach is to use a simple functional helper to automate the creation of type-safe action creators. The advantage is that we can reduce a lot of code repetition and also minimize surface of errors by using type-checked API.
There are more specialized functional helpers available that will help you to further reduce tedious boilerplate and type-annotations in common scenarios like reducers (using
getType) or epics (usingisActionOf).
All that without losing type-safety! Please check this very shortTutorial
import{createAction}from'typesafe-actions';exportconstcountersActions={increment:createAction('INCREMENT'),add:createAction('ADD',(amount:number)=>({type:'ADD',payload:amount,})),};
show usage
importstorefrom'@src/store';import{countersActions}from'@src/redux/counters';// store.dispatch(countersActions.increment(1)); // Error: Expected 0 arguments, but got 1.store.dispatch(countersActions.increment());// OK => { type: "INCREMENT" }
Declare reducerState type withreadonly modifier to get "type level" immutability
exporttypeState={readonlycounter:number,};
Readonly modifier allow initialization, but will not allow rassignment by highlighting compiler errors
exportconstinitialState:State={counter:0,};// OKinitialState.counter=3;// Error, cannot be mutated
This means that thereadonly modifier doesn't propagate immutability down to "properties" of objects. You'll need to set it explicitly on each nested property that you want.
Check the example below:
exporttypeState={readonlycontainerObject:{readonlyimmutableProp:number,mutableProp:number,}};state.containerObject={mutableProp:1};// Error, cannot be mutatedstate.containerObject.immutableProp=1;// Error, cannot be mutatedstate.containerObject.mutableProp=1;// OK! No error, can be mutated
use
ReadonlyorReadonlyArrayMapped types
exporttypeState=Readonly<{counterPairs:ReadonlyArray<Readonly<{immutableCounter1:number,immutableCounter2:number,}>>,}>;state.counterPairs[0]={immutableCounter1:1,immutableCounter2:1};// Error, cannot be mutatedstate.counterPairs[0].immutableCounter1=1;// Error, cannot be mutatedstate.counterPairs[0].immutableCounter2=1;// Error, cannot be mutated
There is a new (work in progress) feature calledConditional Types, that will allow
ReadonlyRecursivemapped type
using type inference withDiscriminated Union types
import{combineReducers}from'redux';import{getType}from'typesafe-actions';import{ITodo,ITodosFilter}from'./types';import{addTodo,toggleTodo,changeFilter}from'./actions';exporttypeTodosState={readonlyisFetching:boolean;readonlyerrorMessage:string|null;readonlytodos:ITodo[];readonlytodosFilter:ITodosFilter;};exporttypeRootState={todos:TodosState;};exportconsttodosReducer=combineReducers<TodosState,TodosAction>({isFetching:(state=false,action)=>{switch(action.type){default:returnstate;}},errorMessage:(state='',action)=>{switch(action.type){default:returnstate;}},todos:(state=[],action)=>{switch(action.type){casegetType(addTodo):return[...state,action.payload];casegetType(toggleTodo):returnstate.map((item)=>item.id===action.payload ?{ ...item,completed:!item.completed} :item);default:returnstate;}},todosFilter:(state='',action)=>{switch(action.type){casegetType(changeFilter):returnaction.payload;default:returnstate;}},});// inferring union type of actionsimport{$call}from'utility-types';import*asactionsfrom'./actions';constreturnsOfActions=Object.values(actions).map($call);exporttypeTodosAction=typeofreturnsOfActions[number];
import{todosReducer,TodosState,TodosAction}from'./reducer';import{addTodo,changeFilter,toggleTodo}from'./actions';/** * FIXTURES */constactiveTodo={id:'1',completed:false,title:'active todo'};constcompletedTodo={id:'2',completed:true,title:'completed todo'};constinitialState=todosReducer(undefined,{});/** * SCENARIOS */describe('Todos Logic',()=>{describe('initial state',()=>{it('should match a snapshot',()=>{expect(initialState).toMatchSnapshot();});});describe('adding todos',()=>{it('should add a new todo as the first active element',()=>{constaction=addTodo('new todo');conststate=todosReducer(initialState,action);expect(state.todos).toHaveLength(1);expect(state.todos[0].id).toEqual(action.payload.id);});});describe('toggling completion state',()=>{it('should mark as complete todo with id "1"',()=>{constaction=toggleTodo(activeTodo.id);conststate0={ ...initialState,todos:[activeTodo]};expect(state0.todos[0].completed).toBeFalsy();conststate1=todosReducer(state0,action);expect(state1.todos[0].completed).toBeTruthy();});});});
Can be imported in connected components to provide type-safety to Reduxconnect function
import{combineReducers}from'redux';import{routerReducer,RouterState}from'react-router-redux';import{countersReducer,CountersState}from'@src/redux/counters';import{todosReducer,TodosState}from'@src/redux/todos';interfaceStoreEnhancerState{}exportinterfaceRootStateextendsStoreEnhancerState{router:RouterState;counters:CountersState;todos:TodosState;}import{RootAction}from'@src/redux';exportconstrootReducer=combineReducers<RootState,RootAction>({router:routerReducer,counters:countersReducer,todos:todosReducer,});
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics
// RootActionsimport{RouterAction,LocationChangeAction}from'react-router-redux';import{$call}from'utility-types';import{countersActions}from'@src/redux/counters';import{todosActions}from'@src/redux/todos';import{toastsActions}from'@src/redux/toasts';constreturnsOfActions=[ ...Object.values(countersActions), ...Object.values(todosActions), ...Object.values(toastsActions),].map($call);typeAppAction=typeofreturnsOfActions[number];typeReactRouterAction=RouterAction|LocationChangeAction;exporttypeRootAction=|AppAction|ReactRouterAction;
When creating the store, use rootReducer. This will set-up astrongly typed Store instance with type inference.
The resulting store instance methods like
getStateordispatchwill be type checked and expose type errors
import{createStore,applyMiddleware,compose}from'redux';import{createEpicMiddleware}from'redux-observable';import{rootReducer,rootEpic,RootState}from'@src/redux';constcomposeEnhancers=(process.env.NODE_ENV==='development'&&window&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)||compose;functionconfigureStore(initialState?:RootState){// configure middlewaresconstmiddlewares=[createEpicMiddleware(rootEpic),];// 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;
UseisActionOf helper to filter actions and to narrowRootAction union type to a specific "action type" down the stream.
import{combineEpics,Epic}from'redux-observable';import{isActionOf}from'typesafe-actions';import{Observable}from'rxjs/Observable';importcuidfrom'cuid';import{RootAction,RootState}from'@src/redux';import{todosActions}from'@src/redux/todos';import{toastsActions}from'./';constTOAST_LIFETIME=2000;constaddTodoToast:Epic<RootAction,RootState>=(action$,store)=>action$.filter(isActionOf(todosActions.addTodo)).concatMap((action)=>{// action is type: { type: "ADD_TODO"; payload: string; }consttoast={id:cuid(),text:action.payload.title};constaddToast$=Observable.of(toastsActions.addToast(toast));constremoveToast$=Observable.of(toastsActions.removeToast(toast.id)).delay(TOAST_LIFETIME);returnaddToast$.concat(removeToast$);});exportconstepics=combineEpics(addTodoToast);
import{createSelector}from'reselect';import{RootState}from'@src/redux';exportconstgetTodos=(state:RootState)=>state.todos.todos;exportconstgetTodosFilter=(state:RootState)=>state.todos.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;}},);
This pattern is focused on a KISS principle - to stay clear of abstractions and to follow a more complex but familiar JavaScript "const" based approach:
Advantages:
- familiar to standard JS "const" based approach
Disadvantages:
- significant amount of boilerplate and duplication
- more complex compared to
createActionhelper library - necessary to export both action types and action creators to re-use in other places, e.g.
redux-sagaorredux-observable
exportconstINCREMENT='INCREMENT';exportconstADD='ADD';exporttypeActions={INCREMENT:{type:typeofINCREMENT,},ADD:{type:typeofADD,payload:number,},};exporttypeRootAction=Actions[keyofActions];exportconstactions={increment:():Actions[typeofINCREMENT]=>({type:INCREMENT,}),add:(amount:number):Actions[typeofADD]=>({type:ADD,payload:amount,}),};
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
{"extends":["tslint:recommended","tslint-react"],"rules":{"arrow-parens":false,"arrow-return-shorthand":[false],"comment-format":[true,"check-space"],"import-blacklist":[true,"rxjs"],"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-null-keyword":false,"no-require-imports":false,"no-submodule-imports":[true,"@src","rxjs"],"no-this-assignment":[true,{"allow-destructuring":true}],"no-trailing-whitespace":true,"no-unused-variable":[true,"react"],"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"],"trailing-comma":[true,{"singleline":"never","multiline":{"objects":"always","arrays":"always","functions":"never","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-lambda":false}}
Installation
npm i -D jest ts-jest @types/jest
{"verbose":true,"transform": {".(ts|tsx)":"./node_modules/ts-jest/preprocessor.js" },"testRegex":"(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$","moduleFileExtensions": ["ts","tsx","js"],"moduleNameMapper": {"^Components/(.*)":"./src/components/$1" },"globals": {"window": {},"ts-jest": {"tsConfigFile":"./tsconfig.json" } },"setupFiles": ["./jest.stubs.js" ],"setupTestFrameworkScriptFile":"./jest.tests.js"}// Global/Window object Stubs for Jestwindow.requestAnimationFrame=function(callback){setTimeout(callback);};window.localStorage={getItem:function(){},setItem:function(){},};Object.values=()=>[];
Installation
npm i -D enzyme enzyme-adapter-react-16 @types/enzyme
import{configure}from'enzyme';importAdapterfrom'enzyme-adapter-react-16';configure({adapter:newAdapter()});
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 setup for best benefits from type-checking, with support for JSX and ES2016 features
- Add
tslibto minimize bundle size:npm i tslib- this will externalize helper functions generated by transpiler and otherwise inlined in your modules - Include absolute imports config working with Webpack
{"compilerOptions":{"baseUrl":"./",// enables absolute path imports"paths":{// define absolute path mappings"@src/*":["src/*"]// will enable -> import { ... } from '@src/components'// in webpack you need to add -> resolve: { alias: { '@src': PATH_TO_SRC }}},"outDir":"dist/",// target for compiled files"allowSyntheticDefaultImports":true,// no errors on commonjs default import"allowJs":true,// include js files"checkJs":true,// typecheck js files"declaration":false,// don't emit declarations"emitDecoratorMetadata":true,"experimentalDecorators":true,"forceConsistentCasingInFileNames":true,"importHelpers":true,// importing helper functions from tslib"noEmitHelpers":true,// disable emitting inline helper functions"jsx":"react",// process JSX"lib":["dom","es2016","es2017.object"],"target":"es5",// "es2015" for ES6+ engines"module":"commonjs",// "es2015" for tree-shaking"moduleResolution":"node","noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitAny":true,"noImplicitReturns":true,"noImplicitThis":true,"noUnusedLocals":true,"strict":true,"pretty":true,"removeComments":true,"sourceMap":true},"include":["src/**/*"],"exclude":["node_modules","src/**/*.spec.*"]}
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.SFC<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';...
Strategies to fix issues coming from broken "vendor type declarations" 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 declarations for vendor modules you can "assert" a module type withany usingShorthand Ambient Modules
//@src/types/modules.d.tsdeclare module'react-test-renderer';declare module'enzyme';
More advanced scenarios for working with vendor module declarations can be found hereOfficial TypeScript Docs
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
classStatefulCounterWithInitialCountextendsReact.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
classStatefulCounterextendsReact.Component<Props,State>{// handlers using Class Fields with arrow functionshandleIncrement=()=>{this.setState({count:this.state.count+1});}; ...}
- Don't edit
README.md- it is built withgeneratorscript from separate.mdfiles located in the/docs/markdownfolder, edit them instead - For code snippets, they are also injected by
generatorscript from the source files located in the playground folder (this step make sure all examples are type-checked and linted), edit them instead
look for include directives in
.mdfiles that look like this:::[example|usage]='../../playground/src/components/sfc-counter.tsx'::
Before opening PR please make sure to check:
# run linter in playgroundyarn run lint# run type-checking in playgroundyarn run tsc# re-generate `README.md` from repo rootsh ./generate.sh# ornode ./generator/bin/generate-readme.js
https://github.com/piotrwitek/react-redux-typescript-webpack-starter
Curated list of relevant in-depth tutorials
Higher-Order Components:
About
The complete guide to static typing in "React & Redux" apps using TypeScript
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Languages
- TypeScript91.8%
- JavaScript6.9%
- Other1.3%