Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

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

Provide feedback

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

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

nulldeba/react-redux-typescript-guide

 
 

Repository files navigation

"This guide is aliving compendium documenting the most important patterns and recipes on how to useReact (and its Ecosystem) in afunctional style 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."

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

Found it useful? Want more updates?Show your support by giving a ⭐

The Mighty Tutorial for completely typesafe Redux Architecture 📖

Reference implementation of Todo-App withtypesafe-actions:https://codesandbox.io/s/github/piotrwitek/typesafe-actions-todo-app 💻

Now compatible withTypeScript v2.8.3 (rewritten using conditional types) 🎉

Goals

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

Complementary Projects

Playground Project

Codeship Status for piotrwitek/react-redux-typescript-guide

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.

Contributing Guide

If you're planning to contribute please make sure to read the contributing guide:CONTRIBUTING.md

Sponsor

If you like what we're doing here, you can help us by funding the work on specific issues that you choose by using IssueHunt.io!

This gives you the power to prioritize our work and support the project contributors. Moreover it'll guarantee the project will be updated and maintained in the long run.

Sponsors are listed in the contributors section at the bottom. If you want to be removed please contact me at:piotrek.witek@gmail.com

issuehunt-image


Table of Contents


Type Definitions & Complementary Libraries

Type Definitions for React & Redux

npm i -D @types/react @types/react-dom @types/react-redux

"react" -@types/react
"react-dom" -@types/react-dom
"redux" - (types included with npm package)*
"react-redux" -@types/react-redux

*NB: Guide is based on types from Redux v4.x.x (Beta). To make it work with Redux v3.x.x please refer tothis config)

Complementary Libraries

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)

⇧ back to top


React Types Cheatsheet

React.StatelessComponent<P> orReact.SFC<P>

Type representing stateless functional component

constMyComponent:React.SFC<MyComponentProps>= ...

⇧ back to top

React.Component<P, S>

Type representing stateful class component

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

⇧ back to top

React.ComponentType<P>

Type representing union type of (SFC | Component)

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

⇧ back to top

React.ReactElement<P> orJSX.Element

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

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

⇧ back to top

React.ReactNode

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

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

⇧ back to top

React.CSSProperties

Type representing style object in JSX (usefull for css-in-js styles)

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

⇧ back to top

React.ReactEventHandler<E>

Type representing generic event handler

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

⇧ back to top

React.MouseEvent<E> |React.KeyboardEvent<E> |React.TouchEvent<E> etc...

Type representing more specific event handler

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

⇧ back to top


Component Typing Patterns

Stateless Components - SFC

- stateless counter

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>);};

⟩⟩⟩ demo

⇧ back to top

- 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>);};

⟩⟩⟩ demo

⇧ back to top


Stateful Components - Class

- stateful counter

import*asReactfrom'react';exportinterfaceStatefulCounterProps{label:string;}interfaceState{readonlycount:number;}exportclassStatefulCounterextendsReact.Component<StatefulCounterProps,State>{readonlystate:State={count:0,};handleIncrement=()=>{this.setState({count:this.state.count+1});}render(){const{ handleIncrement}=this;const{ label}=this.props;const{ count}=this.state;return(<div><span>{label}:{count}</span><buttontype="button"onClick={handleIncrement}>{`Increment`}</button></div>);}}

⟩⟩⟩ demo

⇧ back to top

- with default props

import*asReactfrom'react';exportinterfaceStatefulCounterWithDefaultProps{label:string;initialCount?:number;}interfaceDefaultProps{readonlyinitialCount:number;}interfaceState{readonlycount: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,};readonlystate: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>);}};

⟩⟩⟩ demo

⇧ back to top


Generic Components

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

- generic list

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

⟩⟩⟩ demo

⇧ back to top


Render Props

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

- name provider

simple component using children as a render prop

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

⟩⟩⟩ demo

⇧ back to top

- mouse provider

Mouse component found inRender Props React Docs

import*asReactfrom'react';exportinterfaceMouseProviderProps{render:(state:MouseProviderState)=>React.ReactNode;}interfaceMouseProviderState{readonlyx:number;readonlyy:number;}exportclassMouseProviderextendsReact.Component<MouseProviderProps,MouseProviderState>{readonlystate:MouseProviderState={x:0,y:0};handleMouseMove=(event:React.MouseEvent<HTMLDivElement>)=>{this.setState({x:event.clientX,y:event.clientY,});};render(){return(<divstyle={{height:'100%'}}onMouseMove={this.handleMouseMove}>{/*          Instead of providing a static representation of what <Mouse> renders,          use the `render` prop to dynamically determine what to render.        */}{this.props.render(this.state)}</div>);}}

⟩⟩⟩ demo

⇧ back to top


Higher-Order Components

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

- withState

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/>);}};};
show usage

import*asReactfrom'react';import{withState}from'../hoc';import{SFCCounter}from'../components';constSFCCounterWithState=withState(SFCCounter);exportdefault()=>(<SFCCounterWithStatelabel={'SFCCounterWithState'}/>);

⇧ back to top

- withErrorBoundary

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;}};};
show usage

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>);

⇧ back to top


Redux Connected Components

Caveat withbindActionCreators

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 withdispatch and will be very tedious for the long run. See example below:

const mapDispatchToProps = (dispatch: Dispatch<ActionType>) => ({  onIncrement: () => dispatch(actions.increment()),});

- redux connected counter

importTypesfrom'Types';import{connect}from'react-redux';import{countersActions,countersSelectors}from'../features/counters';import{SFCCounter}from'../components';constmapStateToProps=(state:Types.RootState)=>({count:countersSelectors.getReduxCounter(state.counters),});exportconstSFCCounterConnected=connect(mapStateToProps,{onIncrement:countersActions.increment,})(SFCCounter);
show usage

import*asReactfrom'react';import{SFCCounterConnected}from'../connected';exportdefault()=>(<SFCCounterConnectedlabel={'SFCCounterConnected'}/>);

⇧ back to top

- redux connected counter (verbose)

importTypesfrom'Types';import{bindActionCreators,Dispatch}from'redux';import{connect}from'react-redux';import{countersActions}from'../features/counters';import{SFCCounter}from'../components';constmapStateToProps=(state:Types.RootState)=>({count:state.counters.reduxCounter,});constmapDispatchToProps=(dispatch:Dispatch<Types.RootAction>)=>bindActionCreators({onIncrement:countersActions.increment,},dispatch);exportconstSFCCounterConnectedVerbose=connect(mapStateToProps,mapDispatchToProps)(SFCCounter);
show usage

import*asReactfrom'react';import{SFCCounterConnectedVerbose}from'../connected';exportdefault()=>(<SFCCounterConnectedVerboselabel={'SFCCounterConnectedVerbose'}/>);

⇧ back to top

- with own props

importTypesfrom'Types';import{connect}from'react-redux';import{countersActions,countersSelectors}from'../features/counters';import{SFCCounter}from'../components';exportinterfaceSFCCounterConnectedExtendedProps{initialCount:number;}constmapStateToProps=(state:Types.RootState,ownProps:SFCCounterConnectedExtendedProps)=>({count:countersSelectors.getReduxCounter(state.counters)+ownProps.initialCount,});exportconstSFCCounterConnectedExtended=connect(mapStateToProps,{onIncrement:countersActions.increment,})(SFCCounter);
show usage

import*asReactfrom'react';import{SFCCounterConnectedExtended}from'../connected';exportdefault()=><SFCCounterConnectedExtendedlabel={'SFCCounterConnectedExtended'}initialCount={10}/>;

⇧ back to top


Redux

Action Creators

We'll be using a battle-tested libraryNPM Downloadsthat automates and simplify maintenace oftype annotations in Redux Architecturestypesafe-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);// });
show usage

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

⇧ back to top


Reducers

State with Type-level Immutability

Declare reducerState type withreadonly modifier to get compile time immutability

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

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

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

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

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

Caveat: Readonly is not recursive

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

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

Best-practices for nested immutability

useReadonly orReadonlyArrayMapped types

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

⇧ back to top

Typing reducer

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

import{combineReducers}from'redux';import{ActionType}from'typesafe-actions';import{Todo,TodosFilter}from'./models';import*asactionsfrom'./actions';import{ADD,CHANGE_FILTER,TOGGLE}from'./constants';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;}},});

⇧ back to top

Testing reducer

import{todosReducerasreducer,todosActionsasactions}from'./';/** * FIXTURES */constactiveTodo={id:'1',completed:false,title:'active todo'};constcompletedTodo={id:'2',completed:true,title:'completed todo'};constinitialState=reducer(undefined,{}asany);/** * STORIES */describe('Todos Stories',()=>{describe('initial state',()=>{it('should match a snapshot',()=>{expect(initialState).toMatchSnapshot();});});describe('adding todos',()=>{it('should add a new todo as the first element',()=>{constaction=actions.add('new todo');conststate=reducer(initialState,action);expect(state.todos).toHaveLength(1);expect(state.todos[0].id).toEqual(action.payload.id);});});describe('toggling completion state',()=>{it('should mark active todo as complete',()=>{constaction=actions.toggle(activeTodo.id);conststate0={ ...initialState,todos:[activeTodo]};expect(state0.todos[0].completed).toBeFalsy();conststate1=reducer(state0,action);expect(state1.todos[0].completed).toBeTruthy();});});});

⇧ back to top


Store Configuration

Create Global RootState and RootAction Types

RootState - type representing root state-tree

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

RootAction - type representing union type of all action objects

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

import{StateType}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;}

⇧ back to top

Create Store

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

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

import{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;

Async Flow

"redux-observable"

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);

⇧ back to top


Selectors

"reselect"

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

⇧ back to top


Typing connect

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/sfc-counter-connected-verbose.tsx

importTypesfrom'Types';import{bindActionCreators,Dispatch}from'redux';import{connect}from'react-redux';import{countersActions}from'../features/counters';import{SFCCounter,SFCCounterProps}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:SFCCounterProps)=>({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!exportconstSFCCounterConnectedVerbose=connect(mapStateToProps,mapDispatchToProps)(SFCCounter);

⇧ back to top


Tools

TSLint

Installation
npm i -D tslint

tslint.json

  • Recommended setup is to extend build-in presettslint:recommended (usetslint:all to enable all rules)
  • Add additionalreact specific 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}}

⇧ back to top

Jest

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

jest.config.json

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

jest.stubs.js

// Global/Window object Stubs for Jestwindow.requestAnimationFrame=function(callback){setTimeout(callback);};window.localStorage={getItem:function(){},setItem:function(){},};Object.values=()=>[];

⇧ back to top

Enzyme

Installation
npm i -D enzyme enzyme-adapter-react-16 @types/enzyme

jest.tests.js

import{configure}from'enzyme';importAdapterfrom'enzyme-adapter-react-16';configure({adapter:newAdapter()});

⇧ back to top

Living Style Guide

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ back to top

Common Npm Scripts

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",

⇧ back to top


Recipes

tsconfig.json

  • Recommended baseline config carefully optimized for strict type-checking and optimal webpack workflow
  • Installtslib to 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
{"compilerOptions":{"baseUrl":"./",// enables project relative paths config"paths":{// define paths 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 with commonjs modules interop"esModuleInterop":true,"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.*"]}

⇧ back to top

Default and Named Module Exports

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';...

⇧ back to top

Vendor Types Augmentation

Strategies to fix issues coming from broken "vendor type declarations" files (*.d.ts)

Augmenting library internal type declarations - using relative import resolution

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

⇧ back to top

Augmenting library public type declarations - using node module import resolution

// 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>;}}

⇧ back to top

To quick-fix missing type declarations for vendor modules you can "assert" a module type withany usingShorthand Ambient Modules

// typings/modules.d.tsdeclare module'Types';declare module'react-test-renderer';declare module'enzyme';

More advanced scenarios for working with vendor module declarations can be found hereOfficial TypeScript Docs

⇧ back to top


FAQ

- should I still use React.PropTypes in TS?

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.

⇧ back to top

- when to useinterface declarations and whentype aliases?

From practical side, usinginterface declaration will display identity (interface name) in compiler errors, on the contrarytype aliases 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-lint rule:https://palantir.github.io/tslint/rules/interface-over-type-literal/

⇧ back to top

- how to best initialize class instance or static properties?

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,};  ...}

⇧ back to top

- how to best declare component handler functions?

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});};  ...}

⇧ back to top


Tutorials

Curated list of relevant in-depth tutorials

Higher-Order Components:

⇧ back to top


Contributors

Thanks goes to these wonderful people (emoji key):


Piotrek Witek

💻📖🤔👀💡💬

Kazz Yokomizo

💵🔍

Jake Boone

📖

Amit Dahan

📖

gulderov

📖

Erik Pearson

📖

Bryan Mason

📖

Jakub Chodorowicz

💡

Oleg Maslov

🐛

Aaron Westbrook

🐛

Peter Blazejewicz

💡

Solomon White

📖

Levi Rocha

📖

Sudachi-kun

💵

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

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript92.8%
  • JavaScript6.0%
  • Other1.2%

[8]ページ先頭

©2009-2025 Movatter.jp