Skip to content

Latest commit

 

History

History
905 lines (612 loc) · 36.1 KB

README_SOURCE.md

File metadata and controls

905 lines (612 loc) · 36.1 KB

React & Redux in TypeScript - Static Typing Guide

"This guide is a living compendium documenting the most important patterns and recipes on how to use React (and its Ecosystem) in a functional style using TypeScript. It will help you make your code completely type-safe while focusing on inferring the types from implementation so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."

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

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

🎉 Now updated to support TypeScript v3.4 🎉

Buy Me a Coffee Become a Patron

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 with any type)
  • Make type annotations concise by eliminating redundancy in types using advanced TypeScript Language features like Type Inference and Control flow analysis
  • Reduce repetition and complexity of types with TypeScript focused complementary libraries

React, Redux, Typescript Ecosystem

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

Codesandbox links

  • Reference Todo-App implementation using React, Redux, Typescript Guide: Link

Playground Project

Build Status

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

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

Contributing Guide

We are open for contributions. If you're planning to contribute please make sure to read the contributing guide: CONTRIBUTING.md

Funding

This is an independent open-source project created by people investing their free time for the benefit of our community.

If you are using it please consider donating as this will guarantee the project will be updated and maintained in the long run.

Issues can be funded by anyone interested in them being resolved. Reward will be transparently distributed to the contributor handling the task through the IssueHunt platform.

Let's fund issues in this repository


🌟 - New or updated section

Table of Contents


Installation

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 for Redux >= v4.x.x. To make it work with Redux v3.x.x please refer to this config)

⇧ back to top


React - Type-Definitions Cheatsheet

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

Type representing a functional component

const MyComponent: React.FC<Props> = ...

React.Component<Props, State>

Type representing a class component

class MyComponent extends React.Component<Props, State> { ...

React.ComponentType<Props>

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

const withState = <P extends WrappedComponentProps>(
  WrappedComponent: React.ComponentType<P>,
) => { ...

React.ComponentProps<typeof XXX>

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

type MyComponentProps = React.ComponentProps<typeof MyComponent>;

React.ReactElement | JSX.Element

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

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

React.ReactNode

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

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

React.CSSProperties

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

const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...

React.HTMLProps<HTMLXXXElement>

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

const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }

<Input about={...} accept={...} alt={...} ... />

React.ReactEventHandler<HTMLXXXElement>

Type representing generic event handler - for declaring event handlers

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } 

<input onChange={handleChange} ... />

React.XXXEvent<HTMLXXXElement>

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

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

⇧ back to top


React - Typing Patterns

Function Components - FC

- FC counter

::codeblock='playground/src/components/fc-counter.tsx'::

⟩⟩⟩ demo

⇧ back to top

- spread attributes link

::codeblock='playground/src/components/fc-spread-attributes.tsx'::

⟩⟩⟩ demo

⇧ back to top


Class Components

- class counter

::codeblock='playground/src/components/class-counter.tsx'::

⟩⟩⟩ demo

⇧ back to top

- with default props

::codeblock='playground/src/components/class-counter-with-default-props.tsx'::

⟩⟩⟩ 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

::codeblock='playground/src/components/generic-list.tsx'::

⟩⟩⟩ demo

⇧ back to top


Render Props

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

- name provider

simple component using children as a render prop

::codeblock='playground/src/components/name-provider.tsx'::

⟩⟩⟩ demo

⇧ back to top

- mouse provider

Mouse component found in Render Props React Docs

::codeblock='playground/src/components/mouse-provider.tsx'::

⟩⟩⟩ demo

⇧ back to top


Higher-Order Components

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

- withState

Adds state to a stateless counter

::codeblock='playground/src/hoc/with-state.tsx':: ::expander='playground/src/hoc/with-state.usage.tsx'::

⇧ back to top

- withErrorBoundary

Adds error handling using componentDidCatch to any component

::codeblock='playground/src/hoc/with-error-boundary.tsx':: ::expander='playground/src/hoc/with-error-boundary.usage.tsx'::

⇧ back to top


Redux Connected Components

- redux connected counter

::codeblock='playground/src/connected/fc-counter-connected.tsx':: ::expander='playground/src/connected/fc-counter-connected.usage.tsx'::

⇧ back to top

- redux connected counter with own props

::codeblock='playground/src/connected/fc-counter-connected-own-props.tsx':: ::expander='playground/src/connected/fc-counter-connected-own-props.usage.tsx'::

⇧ back to top

- redux connected counter with redux-thunk integration

::codeblock='playground/src/connected/fc-counter-connected-bind-action-creators.tsx':: ::expander='playground/src/connected/fc-counter-connected-bind-action-creators.usage.tsx'::

⇧ back to top

Context

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

ThemeContext

::codeblock='playground/src/context/theme-context.ts'::

⇧ back to top

ThemeProvider

::codeblock='playground/src/context/theme-provider.tsx'::

⇧ back to top

ThemeConsumer

::codeblock='playground/src/context/theme-consumer.tsx'::

ThemeConsumer in class component

::codeblock='playground/src/context/theme-consumer-class.tsx'::

Implementation with Hooks

⇧ back to top

Hooks

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

- useState

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

::codeblock='playground/src/hooks/use-state.tsx'::

⇧ back to top

- useReducer

Hook for state management like Redux in a function component.

::codeblock='playground/src/hooks/use-reducer.tsx'::

⇧ back to top

- useContext

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

::codeblock='playground/src/hooks/use-theme-context.tsx'::

⇧ back to top


Redux - Typing Patterns

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 Redux connect 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

::codeblock='playground/src/store/types.d.ts'::

⇧ back to top

Create Store

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

The resulting store instance methods like getState or dispatch will be type checked and will expose all type errors

::codeblock='playground/src/store/index.ts'::


Action Creators

We'll be using a battle-tested library NPM Downloads that'll help retain complete type soundness and simplify maintenace of types in Redux Architectures typesafe-actions

You can find more real-world examples and in-depth tutorial in: Typesafe-Actions - Tutorial!

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

::codeblock='playground/src/features/counters/actions.ts':: ::expander='playground/src/features/counters/actions.usage.ts'::

⇧ back to top


Reducers

State with Type-level Immutability

Declare reducer State type with readonly modifier to get compile time immutability

export type State = {
  readonly counter: number;
  readonly todos: ReadonlyArray<string>;
};

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

export const initialState: State = {
  counter: 0,
}; // OK

initialState.counter = 3; // TS Error: cannot be mutated

It's great for Arrays 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>'
const newTodos = state.todos.concat('Learn about tagged union types') // OK

Caveat - Readonly is not recursive

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

TIP: use Readonly or ReadonlyArray Mapped types

export type State = Readonly<{
  counterPairs: ReadonlyArray<Readonly<{
    immutableCounter1: number,
    immutableCounter2: number,
  }>>,
}>;

state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // TS Error: cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // TS Error: cannot be mutated

Solution - recursive Readonly is called DeepReadonly

To fix this we can use DeepReadonly type (available from utility-types).

import { DeepReadonly } from 'utility-types';

export type State = DeepReadonly<{
  containerObject: {
    innerValue: number,
    numbers: number[],
  }
}>;

state.containerObject = { innerValue: 1 }; // TS Error: cannot be mutated
state.containerObject.innerValue = 1; // TS Error: cannot be mutated
state.containerObject.numbers.push(1); // TS Error: cannot use mutator methods

⇧ back to top

Typing reducer

to understand following section make sure to learn about Type Inference, Control flow analysis and Tagged union types

::codeblock='playground/src/features/todos/reducer.ts'::

⇧ back to top

Typing reducer with typesafe-actions

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

::codeblock='playground/src/features/todos/reducer-ta.ts'::

⇧ back to top

Testing reducer

::codeblock='playground/src/features/todos/reducer.spec.ts'::

⇧ back to top


Async Flow with redux-observable

Typing epics

::codeblock='playground/src/features/todos/epics.ts'::

⇧ back to top

Testing epics

::codeblock='playground/src/features/todos/epics.spec.ts'::

⇧ back to top


Selectors with reselect

::codeblock='playground/src/features/todos/selectors.ts'::

⇧ back to top


Connect with react-redux

Typing connected component

NOTE: Below you'll find only a short explanation of concepts behind typing connect. For more real-world examples please check Redux Connected Components section.

import MyTypes from 'MyTypes';

import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux';
import { connect } from 'react-redux';

import { countersActions } from '../features/counters';
import { FCCounter } from '../components';

// Type annotation for "state" argument is mandatory to check 
// the correct shape of state object and injected props you can also
// extend connected component Props interface by annotating `ownProps` argument
const mapStateToProps = (state: MyTypes.RootState, ownProps: FCCounterProps) => ({
  count: state.counters.reduxCounter,
});

// "dispatch" argument needs an annotation to check the correct shape
//  of an action object when using dispatch function
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators({
    onIncrement: countersActions.increment,
  }, dispatch);

// shorter alternative is to use an object instead of mapDispatchToProps function
const dispatchToProps = {
    onIncrement: countersActions.increment,
};

// Notice we don't need to pass any generic type parameters to neither
// the connect function below nor map functions declared above
// because type inference will infer types from arguments annotations automatically
// This is much cleaner and idiomatic approach
export const FCCounterConnected =
  connect(mapStateToProps, mapDispatchToProps)(FCCounter);

// You can add extra layer of validation of your action creators
// by using bindActionCreators generic type parameter and RootAction type
const mapDispatchToProps = (dispatch: Dispatch<MyTypes.RootAction>) =>
  bindActionCreators<ActionCreatorsMapObject<Types.RootAction>>({
    invalidActionCreator: () => 1, // Error: Type 'number' is not assignable to type '{ type: "todos/ADD"; payload: Todo; } | { ... }
  }, dispatch);

Typing connected component using redux-thunk action creators

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

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

const thunkAsyncAction = () => async (dispatch: Dispatch): Promise<void> => {
  // dispatch actions, return Promise, etc.
}

const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) =>
  bindActionCreators(
    {
      thunkAsyncAction,
    },
    dispatch
  );

type DispatchProps = ReturnType<typeof mapDispatchToProps>;
// { thunkAsyncAction: () => Promise<void>; }

/* Without "bindActionCreators" fix signature will be the same as the original "unbound" thunk function: */
// { thunkAsyncAction: () => (dispatch: Dispatch<AnyAction>) => Promise<void>; }

⇧ back to top


Configuration & Dev Tools

Common Npm Scripts

Common TS-related npm scripts shared across projects

"prettier": "prettier --list-different 'src/**/*.ts' || (echo '\nPlease fix code formatting by running:\nnpm run prettier:fix\n'; exit 1)",
"prettier:fix": "prettier --write 'src/**/*.ts'",
"lint": "tslint -p ./",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u"
"ci-check": "npm run prettier && npm run lint && npm run tsc && npm run test",

⇧ back to top

TypeScript

We have our own recommended tsconfig.json that you can easily add to your project thanks to react-redux-typescript-scripts package.

tsconfig.json

::expander='playground/tsconfig.json'::

⇧ back to top

TSLib

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

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

Installation
npm i tslib

Then add this to your tsconfig.json:

"compilerOptions": {
  "importHelpers": true
}

⇧ back to top

TSLint

https://palantir.github.io/tslint/

Installation
npm i -D tslint

For React project you should add additional react specific rules: npm i -D tslint-react https://github.com/palantir/tslint-react

We have our own recommended config that you can easily add to your project thanks to react-redux-typescript-scripts package.

tslint.json

::expander='playground/tslint.json'::

⇧ back to top

ESLint

https://eslint.org/
https://typescript-eslint.io

Installation
npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

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

.eslintrc

::expander='playground/.eslintrc'::

⇧ back to top

Jest

https://jestjs.io/

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

jest.config.json

::expander='configs/jest.config.json'::

jest.stubs.js

::expander='configs/jest.stubs.js'::

⇧ back to top

Style Guide

⟩⟩⟩ styleguide.config.js

⟩⟩⟩ demo

⇧ back to top


Recipes

General Tips

- should I still use React.PropTypes in TS?

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

⇧ back to top

- when to use interface declarations and when type aliases?

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

⇧ back to top

- what's better default or named exports?

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

// 1. create your component files (`select.tsx`) using default export in some folder:

// components/select.tsx
const Select: React.FC<Props> = (props) => {
...
export default Select;

// 2. in this folder create an `index.ts` file that will re-export components with named exports:

// components/index.ts
export { default as Select } 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.tsx
import { Select } from '@src/components';
or
import Select from '@src/components/select';
...

⇧ back to top

- how to best initialize class instance or static properties?

Prefered modern syntax is to use class Property Initializers

class ClassCounterWithInitialCount extends React.Component<Props, State> {
  // default props using Property Initializers
  static defaultProps: DefaultProps = {
    className: 'default-class',
    initialCount: 0,
  };
  
  // initial state using Property Initializers
  state: State = {
    count: this.props.initialCount,
  };
  ...
}

⇧ back to top

- how to best declare component handler functions?

Prefered modern syntax is to use Class Fields with arrow functions

class ClassCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
  handleIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };
  ...
}

⇧ back to top

Ambient Modules Tips

Imports in ambient modules

For type augmentation imports should stay outside of module declaration.

import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

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

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

declare module "react-custom-scrollbars" {
    import * as React from "react";
    export interface positionValues {
    ...

⇧ back to top

Type-Definitions Tips

Missing type-definitions error

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

::codeblock='playground/typings/modules.d.ts'::

Using custom d.ts files for npm modules

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

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

⇧ back to top

Type Augmentation Tips

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

Augmenting library internal declarations - using relative import

// added missing autoFocus Prop on Input component in "[email protected]" npm package
declare module '../node_modules/antd/lib/input/Input' {
  export interface InputProps {
    autoFocus?: boolean;
  }
}

Augmenting library public declarations - using node_modules import

// fixed broken public type-definitions in "[email protected]" npm package
import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';

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

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

⇧ back to top


Tutorials & Articles

Curated list of relevant in-depth tutorials

Higher-Order Components:

⇧ back to top


Contributors

Thanks goes to these wonderful people (emoji key):


Piotrek Witek

💻 📖 🤔 👀 💬

Kazz Yokomizo

💵 🔍

Jake Boone

📖

Amit Dahan

📖

gulderov

📖

Erik Pearson

📖

Bryan Mason

📖

Jakub Chodorowicz

💻

Oleg Maslov

🐛

Aaron Westbrook

🐛

Peter Blazejewicz

📖

Solomon White

📖

Levi Rocha

📖

Sudachi-kun

💵

Sosuke Suzuki

💻

Tom Rathbone

📖

Arshad Kazmi

📖

JeongUkJae

📖

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


MIT License

Copyright (c) 2017 Piotr Witek [email protected] (http://piotrwitek.github.io)