“This guide is a recent summary of the most important patterns and examples of how to use React (and the related ecology) in a functional style with TypeScript. It makes your code absolutely type-safe when deducing types from implementations, which reduces the information noise from excessive type declarations and makes it easier to write correct type declarations that are easy to maintain over time.

The target

  • Full type safety (supported--strictPattern) without losing type information (such as missing type assertions or usinganyTo force use)
  • Use advanced TypeScript language features such as type inference and control-flow analysis to eliminate type redundancy and make type declarations concise
  • Use specialized TypeScript complementary libraries to reduce the duplication and complexity of type code

React, Redux, Typescript ecosystems

  • Typesafe-actions – Creators of the Redux/Flux architecture “action-creators” type safety utility set
  • Utility-types – TypeScript’s common collection of generic types that complement TS’s built-in mapping types and aliases – think of it as lodash for type reuse
  • React-redux-typescript – Developer tools configuration file that can be used for projects that follow this guide

The sample

  • Todo-App playground: Codesandbox
  • React, Redux, TypeScript – RealWorld App: Github | Demo

🌟 – New content and updates

directory

  • React – Quick reference table for type definitions

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

  • React.Component<Props, State>

  • React.ComponentType<Props>

  • React.ComponentProps<typeof XXX>

  • React.ReactElement | JSX.Element

  • React.ReactNode

  • React.CSSProperties

  • React.HTMLProps<HTMLXXXElement>

  • React.ReactEventHandler<HTMLXXXElement>

  • React.XXXEvent<HTMLXXXElement>

  • React-type mode

    • Function Components – FC

      • – Counter assembly
      • – Properties of the component are expanded
    • Class Components

      • – Class version of the counter component
      • – Class components and default props
    • Generic components

      • – Generic list components
    • Render Props

      • – Name Specifies the Provider component
      • – Mouse Provider component
    • High order component

      • – Encapsulate a component with HOC
      • — Encapsulate the component with HOC and inject props
      • -Nested hoc-encapsulates the component, props injection, and connects to redux 🌟
    • Redux Connects components

      • – Redux version counter
      • – Redux version of counter with custom props
      • – Redux edition counter, integratedredux-thunk
    • Context

      • ThemeContext
      • ThemeProvider
      • ThemeConsumer
      • ThemeConsumer Class version
    • Hooks

      • – useState
      • – useReducer
      • – useContext
  • Redux – Type mode

    • Store configuration
      • Create the global Store type
      • Create the Store
    • The Action Creators 🌟
    • Reducers
      • Have a State that is immutable at the Type level
      • Reducer Type Declaration
      • usetypesafe-actionsThe Reducer type is declared
      • Test the reducer
    • useredux-observableWriting an asynchronous stream
      • Epics type declaration
      • Test epics
    • usereselectGenerate Selectors
    • usereact-reduxThe connect method
      • Connect component type declarations
      • Connect component type declarations and integrateredux-thunk
  • – Configuration and developer tools

    • General Npm Scripts

    • tsconfig.json

    • TSLib

    • TSLint

    • ESLint

    • Jest

    • Style guide

      • “react-styleguidist”
  • tips

    • General Tips
  • Tips for Module environment declaration

    • Tips on type Definition
  • Tips for Type extension

    • Tutorials and articles

The installation

Type definitions for React & Redux

npm i -D @types/react @types/react-dom @types/react-redux
Copy the code

“react” – @types/react “react-dom” – @types/react-dom “redux” – (types included with npm package)* “react-redux” – @types/react-redux

* Note: The type system of this guide applies to Redux >= V4.x.x. See this configuration if you want to use it for Redux v3.x.x)

Registering back to top


React – Quick reference table for type definitions

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

Represents the type of the function component

const MyComponent: React.FC<Props> = ...
Copy the code

React.Component<Props, State>

Represents the type of a class component

class MyComponent extends React.Component<Props.State> { ...}
Copy the code

React.ComponentType<Props>

Said (the React. FC | React.Com ponent) the type of collection – used in HOC

constwithState = <P extends WrappedComponentProps>( WrappedComponent: React.ComponentType<P>, ) => { ... }Copy the code

React.ComponentProps<typeof XXX>

Gets the Props type of component XXX (warning: cannot be used with statically declared default Props and generic Props).

type MyComponentProps = React.ComponentProps<typeof MyComponent>;
Copy the code

React.ReactElement | JSX.Element

Represents a type of Element concept in React – represents a native DOM component (such as

) or a user-defined composite component (such as
)

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

React.ReactNode

Represents any type of React node (equivalent to ReactElement (including Fragments and Portals) + primitive JS type)

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

React.CSSProperties

Represents the type of style object in JSX – implementing the CSS-in-JS style

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

React.HTMLProps<HTMLXXXElement>

Represents the type of the specified HTML element – used to extend the HTML element

const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props= >{... } <Input about={... } accept={... } alt={... }... />Copy the code

React.ReactEventHandler<HTMLXXXElement>

Generic type representing Event Handler – Used to declare Event handlers

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) = >{... } <input onChange={handleChange} ... />Copy the code

React.XXXEvent<HTMLXXXElement>

Indicates more special events. Some common events are: ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent.

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) = >{... } <div onMouseMove={handleChange} ... />Copy the code

The React.MouseEvent

in the previous code indicates the type of MouseEvent that is mounted on HTMLDivElement.

Registering back to top


React-type mode

Function Components – FC

– Counter assembly

import * as React from 'react';
type Props = {
  label: string;
  count: number;
  onIncrement: () = > void;
};
export const FCCounter: React.FC<Props> = props= > {
  const { label, count, onIncrement } = props;
  const handleIncrement = () = > {
    onIncrement();
  };
  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};
Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


– componentProperties on

import * as React from 'react';
typeProps = { className? :string; style? : React.CSSProperties; };export const FCSpreadAttributes: React.FC<Props> = props= > {
  const{ children, ... restProps } = props;return <div {. restProps} >{children}</div>;
};
Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


Class Components

– Class version of the counter component

import * as React from 'react';
type Props = {
  label: string;
};
type State = {
  count: number;
};
export class ClassCounter extends React.Component<Props.State> {
  readonly 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>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>); }}Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


– Class components and default props

import * as React from 'react';
type Props = {
  label: string;
  initialCount: number;
};
type State = {
  count: number;
};
export class ClassCounterWithDefaultProps extends React.Component<
  Props.State
> {
  static defaultProps = {
    initialCount: 0};readonly state: State = {
    count: this.props.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>
        <button type="button" onClick={handleIncrement}>
          {`Increment`}
        </button>
      </div>); }}Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


Generic components

  • It is easy to generate different types of variant components while reusing common logic
  • A common use case is the generic list component

– Generic list components

import * as React from 'react';
export interface GenericListProps<T> {
  items: T[];
  itemRenderer: (item: T) = > JSX.Element;
}
export class GenericList<T> extends React.Component<GenericListProps<T>, {} > {render() {
    const { items, itemRenderer } = this.props;
    return (
      <div>
        {items.map(itemRenderer)}
      </div>); }}Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


Render Props

Zh-hans.reactjs.org/docs/render…

– Name Specifies the Provider component

Use children as a simple component of render Prop

import * as React from 'react';
interface NameProviderProps {
  children: (state: NameProviderState) = > React.ReactNode;
}
interface NameProviderState {
  readonly name: string;
}
export class NameProvider extends React.Component<NameProviderProps.NameProviderState> {
  readonly state: NameProviderState = { name: 'Piotr' };
  render() {
    return this.props.children(this.state); }}Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


– Mouse Provider component

An example of the Mouse component comes from the Render Props – React document

import * as React from 'react';
export interface MouseProviderProps {
  render: (state: MouseProviderState) = > React.ReactNode;
}
interface MouseProviderState {
  readonly x: number;
  readonly y: number;
}
export class MouseProvider extends React.Component<MouseProviderProps.MouseProviderState> {
  readonly state: MouseProviderState = { x: 0.y: 0 };
  handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) = > {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };
  render() {
    return (
      <div style={{ 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>); }}Copy the code

⟩ ⟩ ⟩ demo

Registering back to top


High order component

Zh-hans.reactjs.org/docs/higher…

– Encapsulate a component with HOC

Adds a state to a stateless counter

import React from 'react';
import { Diff } from 'utility-types';
// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () = > void;
}
export constwithState = <BaseProps extends InjectedProps>( BaseComponent: React.ComponentType<BaseProps> ) => { type HocProps = Diff<BaseProps, InjectedProps> & { // here you can extend hoc with new props initialCount? : number; }; type HocState = { readonly count: number; }; return class Hoc extends React.Component<HocProps, HocState> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withState(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; readonly state: HocState = { count: Number(this.props.initialCount) || 0, }; handleIncrement = () => { this.setState({ count: this.state.count + 1 }); }; render() { const { ... restProps } = this.props; const { count } = this.state; return ( <BaseComponent count={count} // injected onIncrement={this.handleIncrement} // injected {... (restProps as BaseProps)} /> ); }}; };Copy the code
import * as React from 'react';
import { withState } from '.. /hoc';
import { FCCounter } from '.. /components';
const FCCounterWithState = withState(FCCounter);
export default() = ><FCCounterWithState label={'FCCounterWithState'} / >;
Copy the code

Registering back to top


— Encapsulate the component with HOC and inject props

Add error handling to any component with componentDidCatch

import React from 'react';
const MISSING_ERROR = 'Error was swallowed during propagation.';
export constwithErrorBoundary = <BaseProps extends {}>( BaseComponent: React.ComponentType<BaseProps> ) => { type HocProps = { // here you can extend hoc with new props }; type HocState = { readonly error: Error | null | undefined; }; return class Hoc extends React.Component<HocProps, HocState> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withErrorBoundary(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; readonly state: HocState = { error: undefined, }; componentDidCatch(error: Error | null, info: object) { this.setState({ error: error || new Error(MISSING_ERROR) }); this.logErrorToCloud(error, info); } logErrorToCloud = (error: Error | null, info: object) => { // TODO: send error report to service provider }; render() { const { children, ... restProps } = this.props; const { error } = this.state; if (error) { return <BaseComponent {... (restProps as BaseProps)} />; } return children; }}; };Copy the code
import React, {useState} from 'react';
import { withErrorBoundary } from '.. /hoc';
import { ErrorMessage } from '.. /components';
const ErrorMessageWithErrorBoundary =
  withErrorBoundary(ErrorMessage);
const BrokenComponent = () = > {
  throw new Error('I\'m broken! Don\'t render me.');
};
const BrokenButton = () = > {
  const [shouldRenderBrokenComponent, setShouldRenderBrokenComponent] =
    useState(false);
  if (shouldRenderBrokenComponent) {
    return <BrokenComponent />;
  }
  return (
    <button
      type="button"
      onClick={()= > {
        setShouldRenderBrokenComponent(true);
      }}
    >
      {`Throw nasty error`}
    </button>
  );
};

export default() = > (<ErrorMessageWithErrorBoundary>
    <BrokenButton />
  </ErrorMessageWithErrorBoundary>
);
Copy the code

Registering back to top


-Nested hoc-encapsulates the component, props injection, and connects to redux 🌟

Add error handling to any component with componentDidCatch

import { RootState } from 'MyTypes';
import React from 'react';
import { connect } from 'react-redux';
import { Diff } from 'utility-types';
import { countersActions, countersSelectors } from '.. /features/counters';
// These props will be injected into the base component
interface InjectedProps {
  count: number;
  onIncrement: () = > void;
}
export constwithConnectedCount = <BaseProps extends InjectedProps>( BaseComponent: React.ComponentType<BaseProps> ) => { const mapStateToProps = (state: RootState) => ({ count: countersSelectors.getReduxCounter(state.counters), }); const dispatchProps = { onIncrement: countersActions.increment, }; type HocProps = ReturnType<typeof mapStateToProps> & typeof dispatchProps & { // here you can extend ConnectedHoc with new props overrideCount? : number; }; class Hoc extends React.Component<HocProps> { // Enhance component name for debugging and React-Dev-Tools static displayName = `withConnectedCount(${BaseComponent.name})`; // reference to original wrapped component static readonly WrappedComponent = BaseComponent; render() { const { count, onIncrement, overrideCount, ... restProps } = this.props; return ( <BaseComponent count={overrideCount || count} // injected onIncrement={onIncrement} // injected {... (restProps as BaseProps)} /> ); } } const ConnectedHoc = connect< ReturnType<typeof mapStateToProps>, typeof dispatchProps, // use "undefined" if NOT using dispatchProps Diff<BaseProps, InjectedProps>, RootState >( mapStateToProps, dispatchProps )(Hoc); return ConnectedHoc; };Copy the code
import * as React from 'react';
import { withConnectedCount } from '.. /hoc';
import { FCCounter } from '.. /components';
const FCCounterWithConnectedCount = withConnectedCount(FCCounter);
export default() = > (<FCCounterWithConnectedCount overrideCount={5} label={'FCCounterWithState'} / >
);
Copy the code

Registering back to top


Redux Connects components

– Redux version counter

import Types from 'MyTypes';
import { connect } from 'react-redux';
import { countersActions, countersSelectors } from '.. /features/counters';
import { FCCounter } from '.. /components';
const mapStateToProps = (state: Types.RootState) = > ({
  count: countersSelectors.getReduxCounter(state.counters),
});
const dispatchProps = {
  onIncrement: countersActions.increment,
};
export const FCCounterConnected = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
Copy the code
import * as React from 'react';
import { FCCounterConnected } from '. ';
export default() = ><FCCounterConnected label={'FCCounterConnected'} / >;
Copy the code

Registering back to top


– Redux version of counter with custom props

import Types from 'MyTypes';
import { connect } from 'react-redux';
import { countersActions, countersSelectors } from '.. /features/counters';
import { FCCounter } from '.. /components';
typeOwnProps = { initialCount? :number;
};
const mapStateToProps = (state: Types.RootState, ownProps: OwnProps) = > ({
  count:
    countersSelectors.getReduxCounter(state.counters) +
    (ownProps.initialCount || 0)});const dispatchProps = {
  onIncrement: countersActions.increment,
};
export const FCCounterConnectedOwnProps = connect(
  mapStateToProps,
  dispatchProps
)(FCCounter);
Copy the code
import * as React from 'react';
import { FCCounterConnectedOwnProps } from '. ';
export default() = > (<FCCounterConnectedOwnProps
    label={'FCCounterConnectedOwnProps'}
    initialCount={10}
  />
);
Copy the code

Registering back to top


– Redux edition counter, integratedredux-thunk

import Types from 'MyTypes';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import * as React from 'react';
import { countersActions } from '.. /features/counters';
// Thunk Action
const incrementWithDelay = () = > async (dispatch: Dispatch): Promise<void> = > {setTimeout(() = > dispatch(countersActions.increment()), 1000);
};
const mapStateToProps = (state: Types.RootState) = > ({
  count: state.counters.reduxCounter,
});
const mapDispatchToProps = (dispatch: Dispatch<Types.RootAction>) = >
  bindActionCreators(
    {
      onIncrement: incrementWithDelay,
    },
    dispatch
  );
type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps> & {
    label: string;
  };
export const FCCounter: React.FC<Props> = props= > {
  const { label, count, onIncrement } = props;
  const handleIncrement = () = > {
    // Thunk action is correctly typed as promise
    onIncrement().then(() = > {
      // ...
    });
  };
  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button type="button" onClick={handleIncrement}>
        {`Increment`}
      </button>
    </div>
  );
};
export const FCCounterConnectedBindActionCreators = connect(
  mapStateToProps,
  mapDispatchToProps
)(FCCounter);
Copy the code
import * as React from 'react';
import { FCCounterConnectedBindActionCreators } from '. ';
export default() = > (<FCCounterConnectedBindActionCreators
    label={'FCCounterConnectedBindActionCreators'} / >
);
Copy the code

Registering back to top


Context

Zh-hans.reactjs.org/docs/contex…

ThemeContext

import * as React from 'react';
export type Theme = React.CSSProperties;
type Themes = {
  dark: Theme;
  light: Theme;
};
export const themes: Themes = {
  dark: {
    color: 'black'.backgroundColor: 'white',},light: {
    color: 'white'.backgroundColor: 'black',}};export type ThemeContextProps = { theme: Theme; toggleTheme? :() = > void };
const ThemeContext = React.createContext<ThemeContextProps>({ theme: themes.light });
export default ThemeContext;
Copy the code

Registering back to top


ThemeProvider

import React from 'react';
import ThemeContext, { themes, Theme } from './theme-context';
import ToggleThemeButton from './theme-consumer';
interface State {
  theme: Theme;
}
export class ThemeProvider extends React.Component<{}, State> {
  readonly state: 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.Provider value={{ theme.toggleTheme}} >
        <ToggleThemeButton />
      </ThemeContext.Provider>); }}Copy the code

Registering back to top


ThemeConsumer

import * as React from 'react';
import ThemeContext from './theme-context';
type Props = {};
export default function ToggleThemeButton(props: Props) {
  return (
    <ThemeContext.Consumer>
      {({ theme, toggleTheme }) => <button style={theme} onClick={toggleTheme} {. props} / >}
    </ThemeContext.Consumer>
  );
}
Copy the code

ThemeConsumer Class version

import * as React from 'react';
import ThemeContext from './theme-context';
type Props = {};
export class ToggleThemeButtonClass extends React.Component<Props> {
  staticcontextType = ThemeContext; context! : React.ContextType<typeof ThemeContext>;
  render() {
    const { theme, toggleTheme } = this.context;
    return (
      <button style={theme} onClick={toggleTheme}>
        Toggle Theme
      </button>); }}Copy the code

Implementation with Hooks

Registering back to top


Hooks

Zh-hans.reactjs.org/docs/hooks-…

– useState

Zh-hans.reactjs.org/docs/hooks-…

import * as React from 'react';
type Props = { initialCount: number };
export default function Counter({initialCount}: Props) {
  const [count, setCount] = React.useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={()= > setCount(initialCount)}>Reset</button>
      <button onClick={()= > setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={()= > setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}
Copy the code

Registering back to top


– useReducer

State management hooks for function components (similar to Redux).

import * as React from 'react';
interface State {
  count: number;
}
type Action = { type: 'reset' } | { type: 'increment' } | { type: 'decrement' };
function reducer(state: State, action: Action) :State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error();
  }
}
interface CounterProps {
  initialCount: number;
}
function Counter({ initialCount }: CounterProps) {
  const [state, dispatch] = React.useReducer(reducer, {
    count: initialCount,
  });
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={()= > dispatch({ type: 'increment' })}>+</button>
      <button onClick={()= > dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
export default Counter;
Copy the code

Registering back to top


– useContext

Zh-hans.reactjs.org/docs/hooks-…

import * as React from 'react';
import ThemeContext from '.. /context/theme-context';
type Props = {};
export default function ThemeToggleButton(props: Props) {
  const { theme, toggleTheme } = React.useContext(ThemeContext);
  return (
    <button onClick={toggleTheme} style={theme} >
      Toggle Theme
    </button>
  );
}
Copy the code

Registering back to top


Redux – Type mode

Store configuration

Create the global Store type

RootState– Indicates the type of the root state tree

This can be used as an import to ensure type safety when connecting components using the Redux Connect method

RootAction– Indicates the type of all action object collections

Can be used as an import to receive and send Redux actions at different levels (reducers, Sagas, or Redux-Observables epics)

import { StateType, ActionType } from 'typesafe-actions';
declare module 'MyTypes' {
  export type Store = StateType<typeof import('./index').default>;
  export type RootAction = ActionType<typeof import('./root-action').default>;
  export type RootState = StateType<ReturnType<typeof import('./root-reducer').default>>;
}
declare module 'typesafe-actions' {
  interface Types {
    RootAction: ActionType<typeof import('./root-action').default>; }}Copy the code

Registering back to top


Create the Store

When creating a Store instance, we don’t need to write any additional types; it automatically creates a type-safe Store instance through type inference.

Methods in the generated Store instance (like getState and Dispatch) will support type checking and expose any type errors.

import { RootAction, RootState, Services } from 'MyTypes';
import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { createBrowserHistory } from 'history';
import { routerMiddleware as createRouterMiddleware } from 'connected-react-router';
import { composeEnhancers } from './utils';
import rootReducer from './root-reducer';
import rootEpic from './root-epic';
import services from '.. /services';
// browser history
export const history = createBrowserHistory();
export const epicMiddleware = createEpicMiddleware<
  RootAction,
  RootAction,
  RootState,
  Services
>({
  dependencies: services,
});
const routerMiddleware = createRouterMiddleware(history);
// configure middlewares
const middlewares = [epicMiddleware, routerMiddleware];
// compose enhancers
constenhancer = composeEnhancers(applyMiddleware(... middlewares));// rehydrate state on app start
const initialState = {};
// create store
const store = createStore(rootReducer(history), initialState, enhancer);
epicMiddleware.run(rootEpic);
// export store singleton instance
export default store;
Copy the code

Registering back to top


The Action Creators 🌟

We will use mature auxiliary librariestypesafe-actions It is designed to be easy to useTypeScriptTo writeRedux.

Check out this advanced Tutorial to learn more: typesafe-actions-tutorial!

The following scheme uses a simple factory function to automatically create the type-safe Action Creators. The aim is to reduce duplicate actions and creators and reduce code maintenance. The result is absolutely type safe, action-creators and actions.

/* eslint-disable */
import { action } from 'typesafe-actions';
import { ADD, INCREMENT } from './constants';
/* SIMPLE API */
export const increment = () = > action(INCREMENT);
export const add = (amount: number) = > action(ADD, amount);
/* ADVANCED API */
// More flexible allowing to create complex actions more easily
// use can use "action-creator" instance in place of "type constant"
// e.g. case getType(increment): return action.payload;
// This will allow to completely eliminate need for "constants" in your application, more info here:
// https://github.com/piotrwitek/typesafe-actions#constants
import { createAction } from 'typesafe-actions';
import { Todo } from '.. /todos/models';
export const emptyAction = createAction(INCREMENT)<void> ();export const payloadAction = createAction(ADD)<number> ();export const payloadMetaAction = createAction(ADD)<number.string> ();export const payloadCreatorAction = createAction(
  'TOGGLE_TODO'.(todo: Todo) = > todo.id
)<string> ();Copy the code
import store from '.. /.. /store';
import { countersActions as counter } 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
Copy the code

Registering back to top


Reducers

Have a State that is immutable at the Type level

Using the readonly modifier to declare the State type in reducer, you can obtain the immutability at compile time

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

The Readonly modifier allows initialization but does not allow reassignment (the compiler prompts an error)

export const initialState: State = {
  counter: 0};// OK
initialState.counter = 3; // TS Error: cannot be mutated
Copy the code

This works well with arrays in JS, because you can use (push, pop, splice,… (concat, Map, slice,…) Such immutable methods are still allowed.

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
Copy the code

Warning –ReadonlyIt’s not recursive

This means that the readonly modifier does not pass down invariance in the nested structure of an object. You need to tag every attribute for every level. (Readonly)

Tip: Use Readonly or ReadonlyArray mapping 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
Copy the code

Solution –ReadonlyThe recursive version ofDeepReadonly

To solve the above problem, we can use the DeepReadonly type (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
Copy the code

Registering back to top


Reducer Type Declaration

To understand the next section, be sure to understand type inference, controlflow-based type analysis, and marking union types

import { combineReducers } from 'redux';
import { ActionType } from 'typesafe-actions';
import { Todo, TodosFilter } from './models';
import * as actions from './actions';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
export type TodosAction = ActionType<typeof actions>;
export type TodosState = Readonly<{
  todos: Todo[]; todosFilter: TodosFilter; } >.const initialState: TodosState = {
  todos: [].todosFilter: TodosFilter.All,
};
export default combineReducers<TodosState, TodosAction>({
  todos: (state = initialState.todos, action) = > {
    switch (action.type) {
      case ADD:
        return [...state, action.payload];
      case TOGGLE:
        return state.map(item= >
          item.id === action.payload
            ? { ...item, completed: !item.completed }
            : item
        );
      default:
        returnstate; }},todosFilter: (state = initialState.todosFilter, action) = > {
    switch (action.type) {
      case CHANGE_FILTER:
        return action.payload;
      default:
        returnstate; }}});Copy the code

Registering back to top


usetypesafe-actionsThe Reducer type is declared

Note that we do not need to use any generic type parameters on the API. This can be compared with the traditional reducer writing method, which is equivalent.

import { combineReducers } from 'redux';
import { createReducer } from 'typesafe-actions';
import { Todo, TodosFilter } from './models';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
export type TodosState = Readonly<{
  todos: Todo[]; todosFilter: TodosFilter; } >.const initialState: TodosState = {
  todos: [].todosFilter: TodosFilter.All,
};
const todos = createReducer(initialState.todos)
  .handleType(ADD, (state, action) = > [...state, action.payload])
  .handleType(TOGGLE, (state, action) = >
    state.map(item= >
      item.id === action.payload
        ? { ...item, completed: !item.completed }
        : item
    )
  );
const todosFilter = createReducer(initialState.todosFilter).handleType(
  CHANGE_FILTER,
  (state, action) = > action.payload
);
export default combineReducers({
  todos,
  todosFilter,
});
Copy the code

Registering back to top


Test the reducer

import {
  todosReducer as reducer,
  todosActions as actions,
  TodosState,
} from '/';
/** * FIXTURES */
const getInitialState = (initial? : Partial
       ) = >
  reducer(initial as TodosState, {} as any);
/** * STORIES */
describe('Todos Stories'.() = > {
  describe('initial state'.() = > {
    it('should match a snapshot'.() = > {
      const initialState = getInitialState();
      expect(initialState).toMatchSnapshot();
    });
  });
  describe('adding todos'.() = > {
    it('should add a new todo as the first element'.() = > {
      const initialState = getInitialState();
      expect(initialState.todos).toHaveLength(0);
      const state = 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'.() = > {
      const activeTodo = { id: '1'.completed: false.title: 'active todo' };
      const initialState = getInitialState({ todos: [activeTodo] });
      expect(initialState.todos[0].completed).toBeFalsy();
      const state1 = reducer(initialState, actions.toggle(activeTodo.id));
      expect(state1.todos[0].completed).toBeTruthy();
    });
  });
});
Copy the code

Registering back to top


useredux-observableWriting an asynchronous stream

Epics type declaration

import { RootAction, RootState, Services } from 'MyTypes';
import { Epic } from 'redux-observable';
import { tap, ignoreElements, filter } from 'rxjs/operators';
import { isOfType } from 'typesafe-actions';
import { todosConstants } from '.. /todos';
// contrived example!!!
export const logAddAction: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { logger }) = >
  action$.pipe(
    filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; }
    tap(action= > {
      logger.log(
        `action type must be equal: ${todosConstants.ADD}= = =${action.type}`
      );
    }),
    ignoreElements()
  );
Copy the code

Registering back to top


Test epics

import { StateObservable, ActionsObservable } from 'redux-observable';
import { RootState, Services, RootAction } from 'MyTypes';
import { Subject } from 'rxjs';
import { add } from './actions';
import { logAddAction } from './epics';
// Simple typesafe mock of all the services, you dont't need to mock anything else
// It is decoupled and reusable for all your tests, just put it in a separate file
const services = {
  logger: {
    log: jest.fn<Services['logger'] ['log']>(),
  },
  localStorage: {
    loadState: jest.fn<Services['localStorage'] ['loadState'] > (),saveState: jest.fn<Services['localStorage'] ['saveState'] > (),}}; describe('Todos Epics'.() = > {
  let state$: StateObservable<RootState>;
  beforeEach(() = > {
    state$ = new StateObservable<RootState>(
      new Subject<RootState>(),
      undefined as any
    );
  });
  describe('logging todos actions'.() = > {
    beforeEach(() = > {
      services.logger.log.mockClear();
    });
    it('should call the logger service when adding a new todo'.done= > {
      const addTodoAction = add('new todo');
      const action$ = ActionsObservable.of(addTodoAction);
      logAddAction(action$, state$, services)
        .toPromise()
        .then((outputAction: RootAction) = > {
          expect(services.logger.log).toHaveBeenCalledTimes(1);
          expect(services.logger.log).toHaveBeenCalledWith(
            'action type must be equal: todos/ADD === todos/ADD'
          );
          // expect output undefined because we're using "ignoreElements" in epic
          expect(outputAction).toEqual(undefined);
          done();
        });
    });
  });
});
Copy the code

Registering back to top


usereselectGenerate Selectors

import { createSelector } from 'reselect';
import { TodosState } from './reducer';
export const getTodos = (state: TodosState) = > state.todos;
export const getTodosFilter = (state: TodosState) = > state.todosFilter;
export const getFilteredTodos = createSelector(getTodos, getTodosFilter, (todos, todosFilter) = > {
  switch (todosFilter) {
    case 'completed':
      return todos.filter(t= > t.completed);
    case 'active':
      return todos.filter(t= >! t.completed);default:
      returntodos; }});Copy the code

Registering back to top


usereact-reduxThe connect method

Connect component type declarations

Pay attention toIn the following code, there is only a brief description of the concepts behind the connect type declaration. Please see theRedux Connects componentsChapter for more specific examples

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);
Copy the code

Registering back to top


Connect component type declarations and integrateredux-thunk

Pay attention to: With Thunk Action Creators, you need to applybindActionCreators. Only then can you get the correct Dispatch props type signature, as shown below.

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>; }
Copy the code

Registering back to top


Configuration and developer tools

General Npm Scripts

Generic, cross-project, TS-related NPM scripts

"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".Copy the code

Registering back to top


tsconfig.json

We have a recommended tsconfig.json configuration file that you can easily add to your projects using react-redux-typescript-scripts.

{
  "include": [
    "src"."typings"]."exclude": [
    "src/**/*.spec.*"]."extends": "./node_modules/react-redux-typescript-scripts/tsconfig.json"."compilerOptions": {}}Copy the code

Registering back to top


TSLib

www.npmjs.com/package/tsl…

This library reduces the size of your package files by externalizing the runtime helper functions instead of embedding them into each file.

The installation

npm i tslib
Copy the code

Add this line to your tsconfig.json:

"compilerOptions": {
  "importHelpers": true
}
Copy the code

Registering back to top


TSLint

palantir.github.io/tslint/

The installation

npm i -D tslint
Copy the code

For React projects, you should add an additional React rule set: NPM i-d tslint-react github.com/palantir/ts…

We have recommended profiles that you can easily add to your projects using react-redux-typescript scripts.

tslint.json

{
  "extends": [
    "./node_modules/react-redux-typescript-scripts/tslint.json"."./node_modules/react-redux-typescript-scripts/tslint-react.json"]."rules": {
    // you can further customize options here}}Copy the code

Registering back to top


ESLint

eslint.org/ typescript-eslint.io

The installation

npm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Copy the code

We have recommended configuration files that automatically add TypeScript parsers and plugins that you can easily add to your projects using react-Redux-typescript scripts.

.eslintrc

{
  "extends": [
    "react-app"."./node_modules/react-redux-typescript-scripts/eslint.js"]."rules": {
    // you can further customize options here}}Copy the code

Registering back to top


Jest

jestjs.io/

The installation

npm i -D jest ts-jest @types/jest
Copy the code

jest.config.json

{
  "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/"
}
Copy the code

jest.stubs.js

// Global/Window object Stubs for Jest
window.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 = () = > [];
Copy the code

Registering back to top


Style guide

“react-styleguidist”

⟩ ⟩ ⟩ styleguide. Config. Js

⟩ ⟩ ⟩ demo

Registering back to top


tips

General Tips

– Do I still need to use React.PropTypes when using TS?

Not at all. With TypeScript, there’s no need to use PropTypes. After declaring the Props and State interfaces, you’ll get full autocompletion and encoding security via static type checking. This way, you can directly avoid runtime errors and save a lot of debugging time. As an added bonus, this is also an elegant and standardized way to interpret a component’s public API in source code.

Registering back to top


– When will it be usedinterfaceDeclaration, when to usetypeThe alias?

As a practical matter, using an interface declaration generates an interface namename in case of a compilation error, whereas the Type alias does not generate an id and expands to show all properties and nested types. Although I prefer to use type most of the time, there are times when compilation errors are too verbose to be checked, and I use interface instead to hide the less important type details in the error. Related ts – lint rule: palantir. Making. IO/tslint/rule…

Registering back to top

– Which is better, named exports or default exports?

A common adaptation is to use the folder module pattern, so you can use both named and default imports depending on the situation. The benefits of this solution are that you can achieve better encapsulation and the ability to safely refactor the internal naming and folder structure without affecting your business 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'; .Copy the code

Registering back to top


– What are the best practices for initializing class instances or static properties?

The new syntax is preferred for class attribute initialization

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

Registering back to top


– What are the best practices for declaring component handler methods?

The new syntax is preferred, declaring class method 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}); }; . }Copy the code

Registering back to top


Tips for Module environment declaration

(Ambient and Augmentation)

Imports in the environment declaration

To extend a Module, import should be outside the Module declaration.

import { Operator } from 'rxjs/Operator';
import { Observable } from 'rxjs/Observable';
declare module 'rxjs/Subject' {
  interfaceSubject<T> { lift<R>(operator: Operator<T, R>): Observable<R>; }}Copy the code

When creating a third-party type definition, all imports should be inside the Module declaration; otherwise the imports will be treated as extensions and an error will be reported.

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

Registering back to top


Tips on type Definition

Error with missing type definition

If you can’t find a third-party module’s type declaration, you can either write one yourself or disable type checking for that module with the Ambient Modules function.

// typings/modules.d.ts
declare module 'MyTypes';
declare module 'react-test-renderer';
Copy the code

Use customizations for the NPM moduled.tsfile

If you want to use alternative (custom) type definitions for some NPM modules, you can do so by overriding the Paths field in the compile option.

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

Registering back to top

Tips for Type extension

Policies for handling problems related to external type definition files (*.d.ts)

Extend the internal declaration of the library – using the relative path import

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

Extend the public declaration of the library – 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' {
  interfaceSubject<T> { lift<R>(operator: Operator<T, R>): Observable<R>; }}Copy the code

More advanced scenarios with third-party type definitions can be found in the TypeScript official documentation

Registering back to top


Tutorials and articles

A selection of relevant advanced tutorials

High-order components:

  • medium.com/@jrwebdev/r…

Registering back to top