TypeScript with React

Common Operators and Signatures

Type intersection operator (&)

1
class WithLoading extends React.Component<P & WithLoadingProps> { ... }

Generic function

1
const funcComponent = <P extends object>(Component: React.ComponentType<P>): ... => { ... }

Type cast

A type cast (props as P) is required when passing props forward from TypeScript v3.2 onwards, due to a likely bug in TypeScript.

1
return loading ? <LoadingSpinner /> : <Component {...props as P} />;

Examples

Worth to read:

Higher Order Components

Higher Order Component in JS

1
2
3
4
5
6
7
const withLoading = Component =>
class WithLoading extends React.Component {
render() {
const { loading, ...props } = this.props;
return loading ? <LoadingSpinner /> : <Component {...props} />;
}
};

Higher Order Component in TS

1
2
3
4
5
6
7
8
9
10
11
interface WithLoadingProps {
loading: boolean;
}

const withLoading = <P extends object>(Component: React.ComponentType<P>) =>
class WithLoading extends React.Component<P & WithLoadingProps> {
render() {
const { loading, ...props } = this.props;
return loading ? <LoadingSpinner /> : <Component {...props as P} />;
}
};

Function Higher Order Component in TS

1
2
3
4
5
6
7
const withLoading = <P extends object>(
Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> => ({
loading,
...props
}: WithLoadingProps) =>
loading ? <LoadingSpinner /> : <Component {...props as P} />;

Render Prop Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}

interface MakeCounterProps {
minValue?: number;
maxValue?: number;
children(props: InjectedCounterProps): JSX.Element;
}

interface MakeCounterState {
value: number;
}

class MakeCounter extends React.Component<MakeCounterProps, MakeCounterState> {
state: MakeCounterState = {
value: 0,
};

increment = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.maxValue
? prevState.value
: prevState.value + 1,
}));
};

decrement = () => {
this.setState(prevState => ({
value:
prevState.value === this.props.minValue
? prevState.value
: prevState.value - 1,
}));
};

render() {
return this.props.children({
value: this.state.value,
onIncrement: this.increment,
onDecrement: this.decrement,
});
}
}

make-counter-render-prop.tsx

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface CounterProps extends InjectedCounterProps {
style: React.CSSProperties;
}

const Counter = (props: CounterProps) => (
<div style={props.style}>
<button onClick={props.onDecrement}> - </button>
{props.value}
<button onClick={props.onIncrement}> + </button>
</div>
);

interface WrappedCounterProps extends CounterProps {
minValue?: number;
maxValue?: number;
}

const WrappedCounter = ({
minValue,
maxValue,
...props
}: WrappedCounterProps) => (
<MakeCounter minValue={minValue} maxValue={maxValue}>
{injectedProps => <Counter {...props} {...injectedProps} />}
</MakeCounter>
);

wrapped-counter.tsx

Wrapping Render Prop as HOC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Subtract, Omit } from 'utility-types';
import MakeCounter, { MakeCounterProps, InjectedCounterProps } from './MakeCounter';

type MakeCounterHocProps = Omit<MakeCounterProps, 'children'>;

const makeCounter = <P extends InjectedCounterProps>(
Component: React.ComponentType<P>
): React.SFC<Subtract<P, InjectedCounterProps> & MakeCounterHocProps> => ({
minValue,
maxValue,
...props
}: MakeCounterHocProps) => (
<MakeCounter minValue={minValue} maxValue={maxValue}>
{injectedProps => <Component {...props as P} {...injectedProps} />}
</MakeCounter>
);

Source: https://medium.com/@jrwebdev/react-render-props-in-typescript-b561b00bc67c

React Hooks

Function Components with Hooks in TS example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// import useState next to FunctionComponent
import React, { FunctionComponent, useState } from "react";

// our components props accept a number for the initial value
const Counter: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
// since we pass a number here, clicks is going to be a number.
// setClicks is a function that accepts either a number or a function returning
// a number
const [clicks, setClicks] = useState(initial);
return (
<>
<p>Clicks: {clicks}</p>
<button onClick={() => setClicks(clicks + 1)}>+</button>
<button onClick={() => setClicks(clicks - 1)}>-</button>
</>
);
};

Details about React Hooks with TypeScript

1
2
3
const Counter:FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
...
}

with useState

1
2
3
4
5
6
7
8
9
// explicitly setting the types
const [value, setValue] = (useState < number) | (undefined > undefined);
const [value, setValue] = useState < Array < number >> [];

interface MyObject {
foo: string;
bar?: number;
}
const [value, setValue] = useState <MyObject> { foo: "hello" };

with useRef

1
const inputEl = useRef <HTMLInputElement> null;

with useContext

1
2
type Theme = "light" | "dark";
const ThemeContext = createContext <Theme> "dark";

with useReducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface State {
value: number;
}

type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "incrementAmount", amount: number };

const counterReducer = (state: State, action: Action) => {
switch (action.type) {
case "increment":
return { value: state.value + 1 };
case "decrement":
return { value: state.value - 1 };
case "incrementAmount":
return { value: state.value + action.amount };
default:
throw new Error();
}
};

const [state, dispatch] = useReducer(counterReducer, { value: 0 });

dispatch({ type: "increment" });
dispatch({ type: "decrement" });
dispatch({ type: "incrementAmount", amount: 10 });

// TypeScript compilation error
dispatch({ type: "invalidActionType" });