"This guide is about to teach you how to leverage Type Inference, Generics and other Advanced Types as much as possible to write the minimal amount of type annotations needed for your JavaScript code to be completely Type Safe" - this will make sure you get all the benefits of Static Typing and your productivity won't be slowed down by adding excess type annotations.
Found it usefull? Want more updates? Give it a 🌟
- Complete type safety with
--strict
flag without failing toany
type for the best static-typing experience - Minimize amount of manually writing type declarations by learning to leverage Type Inference
- Reduce repetition and complexity of "Redux" type annotations to a minimum with simple functional utilities
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.
- Setup
- React Types Cheatsheet 🌟 NEW
- Component Typing Patterns
- Redux
- Action Creators 📝 UPDATED
- Reducers 📝 UPDATED
- Store Configuration 📝 UPDATED
- Async Flow 📝 UPDATED
- Selectors
- Tools
- Extras
- FAQ
- Roadmap
- Contribution Guide
- Project Examples
npm i -D @types/react @types/react-dom @types/react-redux utility-types
"react" - @types/react
"react-dom" - @types/react-dom
"redux" - (types included with npm package)*
"react-redux" - @types/react-redux
"utility-types" - usefull utility types used throughout the guide
*NB: Guide uses redux v4.x.x types. For version v3.x.x please check this config)
Stateless functional components
const MyComponent: React.SFC<MyComponentProps> = ...
Statefull class component
class MyComponent extends React.Component<MyComponentProps, State> { ...
Accepts sfc or class components with Generic Props Type
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ...
Accepts any react elements (component instances) and also primitive types
const elementOrPrimitive: React.ReactNode = '' || 0 || false || null || <div /> || <MyComponent />;
Similar in usage to ReactNode but limited to accept only react elements (and not primitive types)
const elementOnly: JSX.Element = <div /> || <MyComponent />;
Type-safety for styles using css-in-js
const styles: React.CSSProperties = { flexDirection: 'row', ...
Type-safe event handlers for JSX
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ...
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>
);
};
- spreading attributes link
import * 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>
);
};
import * as React from 'react';
export interface StatefulCounterProps {
label: string;
}
type State = {
count: number;
};
export class StatefulCounter extends React.Component<StatefulCounterProps, State> {
state: State = {
count: 0,
};
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
const { handleIncrement } = this;
const { label } = this.props;
const { count } = this.state;
return (
<div>
<span>{label}: {count} </span>
<button type="button" onClick={handleIncrement}>
{`Increment`}
</button>
</div>
);
}
}
import * as React from 'react';
export interface StatefulCounterWithDefaultProps {
label: string;
initialCount?: number;
}
interface DefaultProps {
initialCount: number;
}
interface State {
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,
};
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>
);
}
};
- easily create typed component variations and reuse common logic
- common use case is a 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>
);
}
}
- function that takes a component and returns a new component
- a new component will infer Props interface from wrapped Component extended with Props of HOC
- will filter out props specific to HOC, and the rest will be passed through to wrapped component
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 WrappedComponentProps {
count: number;
onIncrement: () => any;
}
export const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>
) => {
// These props will be added to original component type
interface Props {
initialCount?: number;
}
interface State {
count: number;
}
return class WithState extends React.Component<Subtract<P, WrappedComponentProps> & Props, State> {
// Enhance component name for debugging and React-Dev-Tools
static displayName = `withState(${WrappedComponent.name})`;
state: State = {
count: (this.props.initialCount || 0)!,
};
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
const { ...remainingProps } = this.props;
const { count } = this.state;
return (
<WrappedComponent
{...remainingProps}
count={count}
onIncrement={this.handleIncrement}
/>
);
}
};
};
show usage
import * as React from 'react';
import { withState } from '@src/hoc';
import { SFCCounter } from '@src/components';
const SFCCounterWithState =
withState(SFCCounter);
export default (() => (
<SFCCounterWithState label={'SFCCounterWithState'} />
)) as React.SFC<{}>;
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 WrappedComponentProps {
onReset?: () => any;
}
export const withErrorBoundary = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>
) => {
interface Props { }
interface State {
error: Error | null | undefined;
}
return class WithErrorBoundary extends React.Component<Subtract<P, WrappedComponentProps> & Props, State> {
static displayName = `withErrorBoundary(${WrappedComponent.name})`;
state: State = {
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, ...remainingProps } = this.props;
const { error } = this.state;
if (error) {
return (
<WrappedComponent
{...remainingProps}
onReset={this.handleReset}
/>
);
}
return children;
}
};
};
show usage
import * as React from 'react';
import { withErrorBoundary } from '@src/hoc';
import { ErrorMessage } from '@src/components';
const ErrorMessageWithErrorBoundary =
withErrorBoundary(ErrorMessage);
const ErrorThrower = () => (
<button type="button" onClick={() => { throw new Error(`Catch this!`); }}>
{`Throw nasty error`}
</button >
);
export default (() => (
<ErrorMessageWithErrorBoundary>
<ErrorThrower />
</ErrorMessageWithErrorBoundary>
)) as React.SFC<{}>;
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()),
});
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'}
/>
);
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'}
/>
);
import { connect } from 'react-redux';
import { RootState } from '@src/redux';
import { countersActions, countersSelectors } from '@src/redux/counters';
import { SFCCounter } from '@src/components';
export interface SFCCounterConnectedExtended {
initialCount: number;
}
const mapStateToProps = (state: RootState, ownProps: SFCCounterConnectedExtended) => ({
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}
/>
);
Using Typesafe Action Creators helpers for Redux
typesafe-actions
A recommended approach is to use a simple functional helper to automate the creation of type-safe action creators. The advantage is that we can reduce a lot of code repetition and also minimize surface of errors by using type-checked API.
There are more specialized functional helpers available that will help you to further reduce tedious boilerplate and type-annotations in common scenarios like reducers (using
getType
) or epics (usingisActionOf
).
All that without losing type-safety! Please check this very short Tutorial
import { createAction } from 'typesafe-actions';
export const countersActions = {
increment: createAction('INCREMENT'),
add: createAction('ADD', (amount: number) => ({
type: 'ADD',
payload: amount,
})),
};
show usage
import store from '@src/store';
import { countersActions } from '@src/redux/counters';
// store.dispatch(actionCreators.increment(1)); // Error: Expected 0 arguments, but got 1.
store.dispatch(countersActions.increment()); // OK => { type: "INCREMENT" }
Declare reducer State
type with readonly
modifier to get "type level" immutability
export type State = {
readonly counter: number,
};
Readonly modifier allow initialization, but will not allow rassignment by highlighting compiler errors
export const initialState: State = {
counter: 0,
}; // OK
initialState.counter = 3; // Error, cannot be mutated
This means that the readonly
modifier doesn't propagate immutability down to "properties" of objects. You'll need to set it explicitly on each nested property that you want.
Check the example below:
export type State = {
readonly containerObject: {
readonly immutableProp: number,
mutableProp: number,
}
};
state.containerObject = { mutableProp: 1 }; // Error, cannot be mutated
state.containerObject.immutableProp = 1; // Error, cannot be mutated
state.containerObject.mutableProp = 1; // OK! No error, can be mutated
use
Readonly
orReadonlyArray
Mapped types
export type State = Readonly<{
counterPairs: ReadonlyArray<Readonly<{
immutableCounter1: number,
immutableCounter2: number,
}>>,
}>;
state.counterPairs[0] = { immutableCounter1: 1, immutableCounter2: 1 }; // Error, cannot be mutated
state.counterPairs[0].immutableCounter1 = 1; // Error, cannot be mutated
state.counterPairs[0].immutableCounter2 = 1; // Error, cannot be mutated
There is a new (work in progress) feature called Conditional Types, that will allow
ReadonlyRecursive
mapped type
using type inference with Discriminated Union types
import { combineReducers } from 'redux';
import { getType } from 'typesafe-actions';
import { ITodo, ITodosFilter } from './types';
import { addTodo, toggleTodo, changeFilter } from './actions';
export type TodosState = {
readonly isFetching: boolean;
readonly errorMessage: string | null;
readonly todos: ITodo[];
readonly todosFilter: ITodosFilter;
};
export type RootState = {
todos: TodosState;
};
export const todosReducer = combineReducers<TodosState, TodosAction>({
isFetching: (state = false, action) => {
switch (action.type) {
default: return state;
}
},
errorMessage: (state = '', action) => {
switch (action.type) {
default: return state;
}
},
todos: (state = [], action) => {
switch (action.type) {
case getType(addTodo):
return [...state, action.payload];
case getType(toggleTodo):
return state.map((item) => item.id === action.payload
? { ...item, completed: !item.completed }
: item
);
default: return state;
}
},
todosFilter: (state = '', action) => {
switch (action.type) {
case getType(changeFilter):
return action.payload;
default: return state;
}
},
});
// inferring union type of actions
import { $call } from 'utility-types';
import * as actions from './actions';
const returnsOfActions = Object.values(actions).map($call);
export type TodosAction = typeof returnsOfActions[number];
import { todosReducer, TodosState, TodosAction } from './reducer';
import { addTodo, changeFilter, toggleTodo } from './actions';
/**
* FIXTURES
*/
const activeTodo = { id: '1', completed: false, title: 'active todo' };
const completedTodo = { id: '2', completed: true, title: 'completed todo' };
const initialState = todosReducer(undefined, {});
/**
* SCENARIOS
*/
describe('Todos Logic', () => {
describe('initial state', () => {
it('should match a snapshot', () => {
expect(initialState).toMatchSnapshot();
});
});
describe('adding todos', () => {
it('should add a new todo as the first active element', () => {
const action = addTodo('new todo');
const state = todosReducer(initialState, action);
expect(state.todos).toHaveLength(1);
expect(state.todos[0].id).toEqual(action.payload.id);
});
});
describe('toggling completion state', () => {
it('should mark as complete todo with id "1"', () => {
const action = toggleTodo(activeTodo.id);
const state0 = { ...initialState, todos: [activeTodo] };
expect(state0.todos[0].completed).toBeFalsy();
const state1 = todosReducer(state0, action);
expect(state1.todos[0].completed).toBeTruthy();
});
});
});
Can be imported in connected components to provide type-safety to Redux connect
function
import { combineReducers } from 'redux';
import { routerReducer, RouterState } from 'react-router-redux';
import { countersReducer, CountersState } from '@src/redux/counters';
import { todosReducer, TodosState } from '@src/redux/todos';
interface StoreEnhancerState { }
export interface RootState extends StoreEnhancerState {
router: RouterState;
counters: CountersState;
todos: TodosState;
}
import { RootAction } from '@src/redux';
export const rootReducer = combineReducers<RootState, RootAction>({
router: routerReducer,
counters: countersReducer,
todos: todosReducer,
});
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics
// RootActions
import { RouterAction, LocationChangeAction } from 'react-router-redux';
import { $call } from 'utility-types';
import { countersActions } from '@src/redux/counters';
import { todosActions } from '@src/redux/todos';
import { toastsActions } from '@src/redux/toasts';
const returnsOfActions = [
...Object.values(countersActions),
...Object.values(todosActions),
...Object.values(toastsActions),
].map($call);
type AppAction = typeof returnsOfActions[number];
type ReactRouterAction = RouterAction | LocationChangeAction;
export type RootAction =
| AppAction
| ReactRouterAction;
When creating the store, use rootReducer. This will set-up a strongly typed Store instance with type inference.
The resulting store instance methods like
getState
ordispatch
will be type checked and expose type errors
import { createStore, applyMiddleware, compose } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { rootReducer, rootEpic, RootState } from '@src/redux';
const composeEnhancers = (
process.env.NODE_ENV === 'development' &&
window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
) || compose;
function configureStore(initialState?: RootState) {
// 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;
Use isActionOf
helper to filter actions and to narrow RootAction
union type to a specific "action type" down the stream.
import { combineEpics, Epic } from 'redux-observable';
import { isActionOf } from 'typesafe-actions';
import { Observable } from 'rxjs/Observable';
import cuid from 'cuid';
import { RootAction, RootState } from '@src/redux';
import { todosActions } from '@src/redux/todos';
import { toastsActions } from './';
const TOAST_LIFETIME = 2000;
const addTodoToast: Epic<RootAction, RootState> =
(action$, store) => action$
.filter(isActionOf(todosActions.addTodo))
.concatMap((action) => { // action is type: { type: "ADD_TODO"; payload: string; }
const toast = { id: cuid(), text: action.payload.title };
const addToast$ = Observable.of(toastsActions.addToast(toast));
const removeToast$ = Observable.of(toastsActions.removeToast(toast.id))
.delay(TOAST_LIFETIME);
return addToast$.concat(removeToast$);
});
export const epics = combineEpics(addTodoToast);
import { createSelector } from 'reselect';
import { RootState } from '@src/redux';
export const getTodos =
(state: RootState) => state.todos.todos;
export const getTodosFilter =
(state: RootState) => state.todos.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;
}
},
);
This pattern is focused on a KISS principle - to stay clear of abstractions and to follow a more complex but familiar JavaScript "const" based approach:
Advantages:
- familiar to standard JS "const" based approach
Disadvantages:
- significant amount of boilerplate and duplication
- more complex compared to
createAction
helper library - necessary to export both action types and action creators to re-use in other places, e.g.
redux-saga
orredux-observable
export const INCREMENT = 'INCREMENT';
export const ADD = 'ADD';
export type Actions = {
INCREMENT: {
type: typeof INCREMENT,
},
ADD: {
type: typeof ADD,
payload: number,
},
};
export type RootAction = Actions[keyof Actions];
export const actions = {
increment: (): Actions[typeof INCREMENT] => ({
type: INCREMENT,
}),
add: (amount: number): Actions[typeof ADD] => ({
type: ADD,
payload: amount,
}),
};
Installation
npm i -D tslint
- 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
}
}
Installation
npm i -D jest ts-jest @types/jest
{
"verbose": true,
"transform": {
".(ts|tsx)": "./node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"moduleFileExtensions": ["ts", "tsx", "js"],
"moduleNameMapper": {
"^Components/(.*)": "./src/components/$1"
},
"globals": {
"window": {},
"ts-jest": {
"tsConfigFile": "./tsconfig.json"
}
},
"setupFiles": [
"./jest.stubs.js"
],
"setupTestFrameworkScriptFile": "./jest.tests.js"
}
// Global/Window object Stubs for Jest
window.requestAnimationFrame = function (callback) {
setTimeout(callback);
};
window.localStorage = {
getItem: function () { },
setItem: function () { },
};
Object.values = () => [];
Installation
npm i -D enzyme enzyme-adapter-react-16 @types/enzyme
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
Common TS-related npm scripts shared across projects
"lint": "tslint -p ./",
"tsc": "tsc -p ./ --noEmit",
"tsc:watch": "tsc -p ./ --noEmit -w",
"pretest": "npm run lint & npm run tsc",
"test": "jest --config jest.config.json",
"test:watch": "jest --config jest.config.json --watch",
"test:update": "jest --config jest.config.json -u",
- Recommended setup for best benefits from type-checking, with support for JSX and ES2016 features
- Add
tslib
to minimize bundle size:npm i tslib
- this will externalize helper functions generated by transpiler and otherwise inlined in your modules - Include absolute imports config working with Webpack
{
"compilerOptions": {
"baseUrl": "./", // enables absolute path imports
"paths": { // define absolute path mappings
"@src/*": ["src/*"] // will enable -> import { ... } from '@src/components'
// in webpack you need to add -> resolve: { alias: { '@src': PATH_TO_SRC } }
},
"outDir": "dist/", // target for compiled files
"allowSyntheticDefaultImports": true, // no errors on commonjs default import
"allowJs": true, // include js files
"checkJs": true, // typecheck js files
"declaration": false, // don't emit declarations
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true, // importing helper functions from tslib
"noEmitHelpers": true, // disable emitting inline helper functions
"jsx": "react", // process JSX
"lib": [
"dom",
"es2016",
"es2017.object"
],
"target": "es5", // "es2015" for ES6+ engines
"module": "commonjs", // "es2015" for tree-shaking
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strict": true,
"pretty": true,
"removeComments": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"src/**/*.spec.*"
]
}
Most flexible solution is to use module folder pattern, because you can leverage both named and default import when you see fit.
Using this solution you'll achieve better encapsulation for internal structure/naming refactoring without breaking your consumer code:
// 1. in `components/` folder create component file (`select.tsx`) with default export:
// components/select.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';
...
Strategies to fix issues coming from broken "vendor type declarations" files (*.d.ts)
// added missing autoFocus Prop on Input component in "antd@2.10.0" npm package
declare module '../node_modules/antd/lib/input/Input' {
export interface InputProps {
autoFocus?: boolean;
}
}
// 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>;
}
}
To quick-fix missing type declarations for vendor modules you can "assert" a module type with any
using Shorthand Ambient Modules
// @src/types/modules.d.ts
declare module 'react-test-renderer';
declare module 'enzyme';
More advanced scenarios for working with vendor module declarations can be found here Official TypeScript Docs
No. With TypeScript, using PropTypes is an unnecessary overhead. When declaring IProps and IState interfaces, you will get complete intellisense and compile-time safety with static type checking. This way you'll be safe from runtime errors and you will save a lot of time on debugging. Additional benefit is an elegant and standardized method of documenting your component external API in the source code.
From practical side, using
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/
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,
};
...
}
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 });
};
...
}
- extend HOC section with more advanced examples #5
- investigate typing patterns for generic component children #7
- 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
https://github.com/piotrwitek/react-redux-typescript-webpack-starter