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 it's Ecosystem) in a functional style with TypeScript and to make your code completely type-safe while focusing on a conciseness of type annotations so it's a minimal effort to write and to maintain types in the long run."
Show your support by giving a ⭐
Found it usefull? Want more updates?
The Mighty Tutorial for completely typesafe Redux Architecture 📖
Reference implementation of Todo-App with
typesafe-actions
: https://codesandbox.io/s/github/piotrwitek/typesafe-actions-todo-app 💻
Now compatible with TypeScript v2.8.3 (rewritten using conditional types) 🎉
Goals
- Complete type safety (with
--strict
flag) without loosing 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 redudancy 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
Complementary Projects
- Typesafe Action Creators for Redux / Flux Architectures typesafe-actions
- Utility Types for TypeScript: utility-types
- Reference implementation of Todo-App: typesafe-actions-todo-app
Playground Project
You should check Playground Project located in the /playground
folder. It is a source of all the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type definitions (like @types/react
or @types/react-redux
) to ensure the examples are up-to-date and not broken with updated definitions.
Playground was created is such a way, that you can simply clone the repository locally and immediately play around on your own to learn all the examples from this guide in a real project environment without the need to create some complicated environment setup by yourself.
Table of Contents
- Type Definitions & Complementary Libraries
- React Types Cheatsheet 🌟 NEW
- Component Typing Patterns
- Redux
- Action Creators 📝 UPDATED
- Reducers 📝 UPDATED
- Store Configuration 📝 UPDATED
- Async Flow 📝 UPDATED
- Selectors
- Tools
- Recipes
- FAQ
- Contribution Guide
- Tutorials
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 to this config)
Complementary Libraries
Utility libraries with 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)
React Types Cheatsheet
React.StatelessComponent<P>
or React.SFC<P>
Type representing stateless functional component
const MyComponent: React.SFC<MyComponentProps> = ...
React.Component<P, S>
Type representing statefull class component
class MyComponent extends React.Component<MyComponentProps, State> { ...
React.ComponentType<P>
Type representing union type of (SFC | Component)
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...
React.ReactElement<P>
or JSX.Element
Type representing a concept of React Element - representation of a native DOM component (
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 (usefull for css-in-js styles)
const styles: React.CSSProperties = { flexDirection: 'row', ...
const element = <div style={styles} ...
React.ReactEventHandler<E>
Type representing generic event handler
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }
<input onChange={handleChange} ... />
React.MouseEvent<E>
| React.KeyboardEvent<E>
| React.TouchEvent<E>
etc...
Type representing more specific event handler
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} ... />
Component Typing Patterns
Stateless Components - SFC
- stateless counter
import * as React from 'react';
export interface SFCCounterProps {
label: string;
count: number;
onIncrement: () => any;
}
export const SFCCounter: React.SFC<SFCCounterProps> = (props) => {
const { label, count, onIncrement } = props;
const handleIncrement = () => { onIncrement(); };
return (
<div>
<span>{label}: {count} </span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
};
link
- spread attributesimport * as React from 'react';
export interface SFCSpreadAttributesProps {
className?: string;
style?: React.CSSProperties;
}
export const SFCSpreadAttributes: React.SFC<SFCSpreadAttributesProps> = (props) => {
const { children, ...restProps } = props;
return (
<div {...restProps}>
{children}
</div>
);
};
Stateful Components - Class
- stateful counter
import * as React from 'react';
export interface StatefulCounterProps {
label: string;
}
interface State {
readonly count: number;
}
export class StatefulCounter extends React.Component<StatefulCounterProps, 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>
);
}
}
- with default props
import * as React from 'react';
export interface StatefulCounterWithDefaultProps {
label: string;
initialCount?: number;
}
interface DefaultProps {
readonly initialCount: number;
}
interface State {
readonly count: number;
}
export const StatefulCounterWithDefault: React.ComponentClass<StatefulCounterWithDefaultProps> =
class extends React.Component<StatefulCounterWithDefaultProps & DefaultProps> {
// to make defaultProps strictly typed we need to explicitly declare their type
// @see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11640
static defaultProps: DefaultProps = {
initialCount: 0,
};
readonly state: State = {
count: this.props.initialCount,
};
componentWillReceiveProps({ initialCount }: StatefulCounterWithDefaultProps) {
if (initialCount != null && initialCount !== this.props.initialCount) {
this.setState({ count: initialCount });
}
}
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
<span>{label}: {count} </span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
}
};
Generic Components
- easily create typed component variations and reuse common logic
- common use case is a generic list components
- generic list
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>
);
}
}
Render Props
- name provider
simple component using children as a 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);
}
}
- mouse provider
Mouse
component found in Render Props React Docs
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>
);
}
}
Higher-Order Components
- withState
Adds state to a stateless counter
import * as React from 'react';
import { Subtract } from 'utility-types';
// These props will be subtracted from original component type
interface InjectedProps {
count: number;
onIncrement: () => any;
}
export const withState = <WrappedProps extends InjectedProps>(
WrappedComponent: React.ComponentType<WrappedProps>
) => {
// These props will be added to original component type
type HocProps = Subtract<WrappedProps, InjectedProps> & {
// here you can extend hoc props
initialCount?: number;
};
type HocState = {
readonly count: number;
};
return class WithState extends React.Component<HocProps, HocState> {
// Enhance component name for debugging and React-Dev-Tools
static displayName = `withState(${WrappedComponent.name})`;
// reference to original wrapped component
static readonly WrappedComponent = WrappedComponent;
readonly state: HocState = {
count: Number(this.props.initialCount) || 0,
};
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { ...restProps } = this.props as {};
const { count } = this.state;
return (
<WrappedComponent
{...restProps}
count={count} // injected
onIncrement={this.handleIncrement} // injected
/>
);
}
};
};
show usage
import * as React from 'react';
import { withState } from '@src/hoc';
import { SFCCounter } from '@src/components';
const SFCCounterWithState =
withState(SFCCounter);
export default () => (
<SFCCounterWithState label={'SFCCounterWithState'} />
);
- withErrorBoundary
Adds error handling using componentDidCatch to any component
import * as React from 'react';
import { Subtract } from 'utility-types';
const MISSING_ERROR = 'Error was swallowed during propagation.';
interface InjectedProps {
onReset: () => any;
}
export const withErrorBoundary = <WrappedProps extends InjectedProps>(
WrappedComponent: React.ComponentType<WrappedProps>
) => {
type HocProps = Subtract<WrappedProps, InjectedProps> & {
// here you can extend hoc props
};
type HocState = {
readonly error: Error | null | undefined;
};
return class WithErrorBoundary extends React.Component<HocProps, HocState> {
static displayName = `withErrorBoundary(${WrappedComponent.name})`;
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 cloud
};
handleReset = () => {
this.setState({ error: undefined });
};
render() {
const { children, ...restProps } = this.props as {
children: React.ReactNode;
};
const { error } = this.state;
if (error) {
return (
<WrappedComponent
{...restProps}
onReset={this.handleReset} // injected
/>
);
}
return children;
}
};
};
show usage
import * as React from 'react';
import { withErrorBoundary } from '@src/hoc';
import { ErrorMessage } from '@src/components';
const ErrorMessageWithErrorBoundary =
withErrorBoundary(ErrorMessage);
const BrokenButton = () => (
<button type="button" onClick={() => { throw new Error(`Catch me!`); }}>
{`Throw nasty error`}
</button >
);
export default () => (
<ErrorMessageWithErrorBoundary>
<BrokenButton />
</ErrorMessageWithErrorBoundary>
);
Redux Connected Components
bindActionCreators
Caveat with If you try to use connect
or bindActionCreators
explicitly and want to type your component callback props as () => void
this will raise compiler errors. It happens because bindActionCreators
typings will not map the return type of action creators to void
, 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 with connect
are also using this pattern.
If there is any progress or fix in regard to the above caveat I'll update the guide and make an announcement on my twitter/medium (There are a few existing proposals already).
There is alternative way to retain type soundness but it requires an explicit wrapping with
dispatch
and will be very tedious for the long run. See example below:
const mapDispatchToProps = (dispatch: Dispatch) => ({
onIncrement: () => dispatch(actions.increment()),
});
- redux connected counter
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { countersActions, countersSelectors } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
const mapStateToProps = (state: RootState) => ({
count: countersSelectors.getReduxCounter(state),
});
export const SFCCounterConnected = connect(mapStateToProps, {
onIncrement: countersActions.increment,
})(SFCCounter);
show usage
import * as React from 'react';
import { SFCCounterConnected } from '@src/connected';
export default () => (
<SFCCounterConnected
label={'SFCCounterConnected'}
/>
);
- redux connected counter (verbose)
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { RootState, Dispatch } from '@src/redux';
import { countersActions } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
const mapStateToProps = (state: RootState) => ({
count: state.counters.reduxCounter,
});
const mapDispatchToProps = (dispatch: Dispatch) => bindActionCreators({
onIncrement: countersActions.increment,
}, dispatch);
export const SFCCounterConnectedVerbose =
connect(mapStateToProps, mapDispatchToProps)(SFCCounter);
show usage
import * as React from 'react';
import { SFCCounterConnectedVerbose } from '@src/connected';
export default () => (
<SFCCounterConnectedVerbose
label={'SFCCounterConnectedVerbose'}
/>
);
- with own props
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { countersActions, countersSelectors } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
export interface SFCCounterConnectedExtendedProps {
initialCount: number;
}
const mapStateToProps = (state: RootState, ownProps: SFCCounterConnectedExtendedProps) => ({
count: countersSelectors.getReduxCounter(state) + ownProps.initialCount,
});
export const SFCCounterConnectedExtended = connect(mapStateToProps, {
onIncrement: countersActions.increment,
})(SFCCounter);
show usage
import * as React from 'react';
import { SFCCounterConnectedExtended } from '@src/connected';
export default () => <SFCCounterConnectedExtended label={'SFCCounterConnectedExtended'} initialCount={10} />;
Redux
Action Creators
We'll be using a battle-tested library that automates and simplify maintenace of type annotations in Redux Architectures
typesafe-actions
The Mighty Tutorial to learn it all the easy way!
You should readA solution below is using simple factory function to automate the creation of type-safe action creators. The goal is to reduce the maintainability and code repetition of type annotations for actions and creators and the result is completely typesafe action-creators and their actions.
import { action, createAction, createStandardAction } from 'typesafe-actions';
import { ADD, INCREMENT } from './constants';
// CLASSIC API
export const increment = () => action(INCREMENT);
export const add = (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):
// export const increment = createStandardAction(INCREMENT)<void>();
// export const add = createStandardAction(ADD)<number>();
// OPTION 2 (with resolve callback):
// export const increment = createAction(INCREMENT);
// export const add = createAction(ADD, resolve => {
// return (amount: number) => resolve(amount);
// });
show usage
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
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 rassignment 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.
To fix this we can use DeepReadonly
type (available in utility-types
npm library - collection of reusable types extending the collection of standard-lib in TypeScript.
Check the example below:
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
Best-practices for nested immutability
use
Readonly
orReadonlyArray
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
Typing reducer
to understand following section make sure to learn about Type Inference, Control flow analysis and Tagged union types
import { combineReducers } from 'redux';
import { ActionsUnion } from 'typesafe-actions';
import { Todo, TodosFilter } from './models';
import * as actions from './actions';
import { ADD, CHANGE_FILTER, TOGGLE } from './constants';
export type TodosState = {
readonly isFetching: boolean;
readonly errorMessage: string | null;
readonly todos: Todo[];
readonly todosFilter: TodosFilter;
};
export type TodosAction = ActionsUnion<typeof actions>;
export default combineReducers<TodosState, TodosAction>({
isFetching: (state = false, action) => {
switch (action.type) {
default:
return state;
}
},
errorMessage: (state = null, action) => {
switch (action.type) {
default:
return state;
}
},
todos: (state = [], 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:
return state;
}
},
todosFilter: (state = TodosFilter.All, action) => {
switch (action.type) {
case CHANGE_FILTER:
return action.payload;
default:
return state;
}
},
});
Testing reducer
import { todosReducer as reducer, todosActions as actions } from './';
/**
* FIXTURES
*/
const activeTodo = { id: '1', completed: false, title: 'active todo' };
const completedTodo = { id: '2', completed: true, title: 'completed todo' };
const initialState = reducer(undefined, {} as any);
/**
* 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', () => {
const action = actions.add('new todo');
const state = 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', () => {
const action = actions.toggle(activeTodo.id);
const state0 = { ...initialState, todos: [activeTodo] };
expect(state0.todos[0].completed).toBeFalsy();
const state1 = reducer(state0, action);
expect(state1.todos[0].completed).toBeTruthy();
});
});
});
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
import { StateType } from 'typesafe-actions';
import { RouterAction, LocationChangeAction } from 'react-router-redux';
type ReactRouterAction = RouterAction | LocationChangeAction;
import { CountersAction } from '../features/counters';
import rootReducer from './root-reducer';
declare module 'Types' {
export type RootState = StateType<typeof rootReducer>;
export type RootAction = ReactRouterAction | CountersAction;
}
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
ordispatch
will be type checked and will expose all type errors
import { createStore, applyMiddleware, compose } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import rootReducer from './root-reducer';
import rootEpic from './root-epic';
const composeEnhancers =
(process.env.NODE_ENV === 'development' &&
window &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
compose;
function configureStore(initialState?: object) {
// configure middlewares
const middlewares = [createEpicMiddleware(rootEpic)];
// compose enhancers
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
// create store
return createStore(rootReducer, initialState!, enhancer);
}
// pass an optional param to rehydrate state on app start
const store = configureStore();
// export store singleton instance
export default store;
Async Flow
"redux-observable"
The Mighty Tutorial to learn it all the easy way!
For more examples and in-depth explanation you should read// tslint:disable:no-console
import Types from '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!!!
const logAddAction: Epic<TodosAction, Types.RootState, Types.Services> = (
action$,
store
) =>
action$.pipe(
filter(isOfType(todosConstants.ADD)), // action is narrowed to: { type: "ADD_TODO"; payload: string; }
tap(action => {
console.log(
`action type must be equal: ${todosConstants.ADD} === ${action.type}`
);
}),
ignoreElements()
);
export default combineEpics(logAddAction);
Selectors
"reselect"
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:
return todos;
}
});
Tools
TSLint
Installation
npm i -D tslint
tslint.json
- Recommended setup is to extend build-in preset
tslint:recommended
(usetslint:all
to enable all rules) - Add additional
react
specific rules:npm i -D tslint-react
https://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
}
}
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 Jest
window.requestAnimationFrame = function (callback) {
setTimeout(callback);
};
window.localStorage = {
getItem: function () { },
setItem: function () { },
};
Object.values = () => [];
Enzyme
Installation
npm i -D enzyme enzyme-adapter-react-16 @types/enzyme
jest.tests.js
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
Living Style Guide
"react-styleguidist"
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",
Recipes
tsconfig.json
- Recommended baseline config carefully optimized for strict type-checking and optimal webpack workflow
- Install
tslib
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.*"
]
}
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.tsx
const Select: React.SFC<Props> = (props) => {
...
export default Select;
// 2. in `components/` folder create `index.ts` file handling named imports:
// 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';
...
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 package
declare module '../node_modules/antd/lib/input/Input' {
export interface InputProps {
autoFocus?: boolean;
}
}
Augmenting library public type declarations - using node module import resolution
// fixed broken public type declaration in "rxjs@5.4.1" 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>;
}
}
any
using Shorthand Ambient Modules
To quick-fix missing type declarations for vendor modules you can "assert" a module type with // typings/modules.d.ts
declare module 'Types';
declare module 'react-test-renderer';
declare module 'enzyme';
More advanced scenarios for working with vendor module declarations can be found here Official TypeScript Docs
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.
interface
declarations and when type
aliases?
- when to use From practical side, using
interface
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/
- how to best initialize class instance or static properties?
Prefered modern style is to use class Property Initializers
class StatefulCounterWithInitialCount 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,
};
...
}
- how to best declare component handler functions?
Prefered modern style is to use Class Fields with arrow functions
class StatefulCounter extends React.Component<Props, State> {
// handlers using Class Fields with arrow functions
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
...
}
Contribution Guide
- Don't edit
README.md
- it is built withgenerator
script from separate.md
files located in the/docs/markdown
folder, edit them instead - For code snippets, they are also injected by
generator
script from the source files located in the playground folder (this step make sure all examples are type-checked and linted), edit them instead
look for include directives in
.md
files that look like this:::[example|usage]='../../playground/src/components/sfc-counter.tsx'::
Before opening PR please make sure to check:
# run linter in playground
yarn run lint
# run type-checking in playground
yarn run tsc
# re-generate `README.md` from repo root
sh ./generate.sh
# or
node ./generator/bin/generate-readme.js
Tutorials
Curated list of relevant in-depth tutorials
Higher-Order Components: