piotrwitek/react-redux-typescript-guide(新しいタブで開きます)
主要言語: TypeScript
The complete guide to static typing in "React & Redux" apps using TypeScript
- star 数
- 13,252
- watcher 数
- 178
- fork 数
- 1,096
- issue 数
- 99
- 最終更新
- 2026/05/09
主要言語: TypeScript
The complete guide to static typing in "React & Redux" apps using TypeScript
"This guide is a living compendium documenting the most important patterns and recipes on how to use React (and its Ecosystem) in a functional style using TypeScript. It will help you make your code completely type-safe while focusing on inferring the types from implementation so there is less noise coming from excessive type annotations and it's easier to write and maintain correct types in the long run."
Found it useful? Want more updates?
Show your support by giving a ⭐
🎉 Now updated to support TypeScript v4.6 🎉
🚀 _Updated to typesafe-actions@5.x 🚀
--strict flag) without losing type information downstream through all the layers of our application (e.g. no type assertions or hacking with any type)Check out our Playground Project located in the /playground folder. It contains all source files of the code examples found in the guide. They are all tested with the most recent version of TypeScript and 3rd party type-definitions (like @types/react or @types/react-redux) to ensure the examples are up-to-date and not broken with updated definitions (It's based on create-react-app --typescript).
Playground project was created so that you can simply clone the repository locally and immediately play around with all the component patterns found in the guide. It will help you to learn all the examples from this guide in a real project environment without the need to create complicated environment setup by yourself.
You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: CONTRIBUTING.md
You can also help by funding issues. Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.
I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.
🌟 - New or updated section
React.FC<Props> | React.FunctionComponent<Props>React.Component<Props, State>React.ComponentType<Props>React.ComponentProps<typeof XXX>React.ReactElement | JSX.ElementReact.ReactNodeReact.CSSPropertiesReact.XXXHTMLAttributes<HTMLXXXElement>React.ReactEventHandler<HTMLXXXElement>React.XXXEvent<HTMLXXXElement>npm i -D @types/react @types/react-dom @types/react-redux
"react" - @types/react
"react-dom" - @types/react-dom
"redux" - (types included with npm package)*
"react-redux" - @types/react-redux
*NB: Guide is based on types for Redux >= v4.x.x.
Type representing a functional component
const MyComponent: React.FC<Props> = ...
Type representing a class component
class MyComponent extends React.Component<Props, State> { ...
Type representing union of (React.FC<Props> | React.Component<Props>) - used in HOC
const withState = <P extends WrappedComponentProps>( WrappedComponent: React.ComponentType<P>, ) => { ...
Gets Props type of a specified component XXX (WARNING: does not work with statically declared default props and generic props)
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
Type representing a concept of React Element - representation of a native DOM component (e.g. <div />), or a user-defined composite component (e.g. <MyComponent />)
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
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 }) => ...
Type representing style object in JSX - for css-in-js styles
const styles: React.CSSProperties = { flexDirection: 'row', ... const element = <div style={styles} ...
Type representing HTML attributes of specified HTML Element - for extending HTML Elements
const Input: React.FC<Props & React.InputHTMLAttributes<HTMLInputElement>> = props => { ... } <Input about={...} accept={...} alt={...} ... />
Type representing generic event handler - for declaring event handlers
const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... } <input onChange={handleChange} ... />
Type representing more specific event. Some common event examples: ChangeEvent, FormEvent, FocusEvent, KeyboardEvent, MouseEvent, DragEvent, PointerEvent, WheelEvent, TouchEvent.
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... } <div onMouseMove={handleChange} ... />
In code above React.MouseEvent<HTMLDivElement> is type of mouse event, and this event happened on HTMLDivElement
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> ); };
import * as React from 'react'; type Props = { label: string; count: number; onIncrement: () => void; }; // React.FC is unaplicable here due not working properly with default props // https://github.com/facebook/create-react-app/pull/8177 export const FCCounterWithDefaultProps = (props: Props): JSX.Element => { const { label, count, onIncrement } = props; const handleIncrement = () => { onIncrement(); }; return ( <div> <span> {label}: {count} </span> <button type="button" onClick={handleIncrement}> {`Increment`} </button> </div> ); }; FCCounterWithDefaultProps.defaultProps = { count: 5 };
import * as React from 'react'; type Props = React.PropsWithChildren<{ className?: string; style?: React.CSSProperties; }>; export const FCSpreadAttributes: React.FC<Props> = (props) => { const { children, ...restProps } = props; return <div {...restProps}>{children}</div>; };
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> ); } }
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> ); } }
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> ); } }
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> </> ); }
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> ); }
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;
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 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> ); } }
Adds 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 const withState = <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 {...(restProps as BaseProps)} count={count} // injected onIncrement={this.handleIncrement} // injected /> ); } }; };
import * as React from 'react'; import { withState } from '../hoc'; import { FCCounter } from '../components'; const FCCounterWithState = withState(FCCounter); export default () => <FCCounterWithState label={'FCCounterWithState'} />;
Adds error handling using componentDidCatch to any component
import React from 'react'; const MISSING_ERROR = 'Error was swallowed during propagation.'; export const withErrorBoundary = <BaseProps extends {}>( BaseComponent: React.ComponentType<BaseProps> ) => { type HocProps = React.PropsWithChildren<{ // 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; } }; };
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> );
Adds error handling using componentDidCatch to any component
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 const withConnectedCount = <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 {...(restProps as BaseProps)} count={overrideCount || count} // injected onIncrement={onIncrement} // injected /> ); } } const ConnectedHoc = connect< ReturnType<typeof mapStateToProps>, typeof dispatchProps, // use "undefined" if NOT using dispatchProps Diff<BaseProps, InjectedProps>, RootState >( mapStateToProps, dispatchProps )(Hoc); return ConnectedHoc; };
import * as React from 'react'; import { withConnectedCount } from '../hoc'; import { FCCounter } from '../components'; const FCCounterWithConnectedCount = withConnectedCount(FCCounter); export default () => ( <FCCounterWithConnectedCount overrideCount={5} label={'FCCounterWithState'} /> );
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);
import * as React from 'react'; import { FCCounterConnected } from '.'; export default () => <FCCounterConnected label={'FCCounterConnected'} />;
import Types from 'MyTypes'; import { connect } from 'react-redux'; import { countersActions, countersSelectors } from '../features/counters'; import { FCCounter } from '../components'; type OwnProps = { 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);
import * as React from 'react'; import { FCCounterConnectedOwnProps } from '.'; export default () => ( <FCCounterConnectedOwnProps label={'FCCounterConnectedOwnProps'} initialCount={10} /> );
import * as React from 'react'; import { FCCounter } from '../components'; import { increment } from '../features/counters/actions'; import { useSelector, useDispatch } from '../store/hooks'; const FCCounterConnectedHooksUsage: React.FC = () => { const counter = useSelector(state => state.counters.reduxCounter); const dispatch = useDispatch(); return <FCCounter label="Use selector" count={counter} onIncrement={() => dispatch(increment())}/>; }; export default FCCounterConnectedHooksUsage;
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);
import * as React from 'react'; import { FCCounterConnectedBindActionCreators } from '.'; export default () => ( <FCCounterConnectedBindActionCreators label={'FCCounterConnectedBindActionCreators'} /> );
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;
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> ); } }
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> ); }
import * as React from 'react'; import ThemeContext from './theme-context'; type Props = {}; export class ToggleThemeButtonClass extends React.Component<Props> { static contextType = ThemeContext; declare context: React.ContextType<typeof ThemeContext>; render() { const { theme, toggleTheme } = this.context; return ( <button style={theme} onClick={toggleTheme}> Toggle Theme </button> ); } }
Can be imported in connected components to provide type-safety to Redux connect function
Can be imported in various layers receiving or sending redux actions like: reducers, sagas or redux-observables epics
import { StateType, ActionType } from 'typesafe-actions'; declare module 'MyTypes' { export type Store = StateType<typeof import('./store').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>; } }
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
getStateordispatchwill be type checked and will expose all type errors
import { RootAction, RootState, Services } from 'MyTypes'; import { applyMiddleware, createStore } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; import services from '../services'; import { routerMiddleware } from './redux-router'; import rootEpic from './root-epic'; import rootReducer from './root-reducer'; import { composeEnhancers } from './utils'; const epicMiddleware = createEpicMiddleware< RootAction, RootAction, RootState, Services >({ dependencies: services, }); // configure middlewares const middlewares = [epicMiddleware, routerMiddleware]; // compose enhancers const enhancer = composeEnhancers(applyMiddleware(...middlewares)); // rehydrate state on app