Overview
- All React frameworks offer support for using TypeScript. Like, Next.js, Remix, Gatsby, Expo, Create-React-App.
- When you want to write React app in TypeScript, you need to setup the configuration to support typeScript. For example, you need
ts-loader
,source-map-loader
,@types
,tsconfig.json
, setup webpack config, etc. - The things you need can be:
npm install --save-dev typescript ts-loader source-map-loader
,npm install @types/react @types/react-dom
etc. - The best way is using framework to build new project which support TypeScript. Otherwise you will have a big head.
- When you are changing your project from JS to TS, it is recommend to use framework to start a new project. You don’t need to care so many setup.
- You will use
tsx
andts
files instead ofjs
files. - Every file containing JSX must use the
.tsx
file extension. This is a TypeScript-specific extension that tells TypeScript that this file contains JSX.
Prop in TypeScript
Writing TypeScript with React is very similar to writing JavaScript with React. The key difference when working with a component is that you can provide types for your component’s props. These types can be used for correctness checking and providing inline documentation in editors.
Inline Object Literals
import { ReactNode } from "react";
const Wrapper = (props: {
children?: ReactNode;
}) => {
return <div>{props.children}</div>;
};
Prop types can also be destructured, leading to a strange {}: {} syntax.
function MyButton({ title }: { title: string }) {
return (
<button>{title}</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton title="I'm a button" />
</div>
);
}
Interfaces and Type aliases
Inline syntax is the simplest way to provide types for a component, though once you start to have a few fields to describe it can become unwieldy. Instead, you can use an interface or type to describe the component’s props.
export interface MyButtonProps {
/** The text to display inside the button */
title: string;
/** Whether the button can be interacted with */
disabled: boolean;
}
function MyButton({ title, disabled }: MyButtonProps) {
return (
<button disabled={disabled}>{title}</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton title="I'm a disabled button" disabled={true}/>
</div>
);
}
Type aliases or interfaces should always be exported along with the component - that way you can use them in other files if needed.
Basic Prop Types
A list of TypeScript types you will likely use in a React+TypeScript app:
type AppProps = {
message: string;
count: number;
disabled: boolean;
/** array of a type! */
names: string[];
/** string literals to specify exact string values, with a union type to join them together */
status: "waiting" | "success";
/** an object with known properties (but could have more at runtime) */
obj: {
id: string;
title: string;
};
/** array of objects! (common) */
objArr: {
id: string;
title: string;
}[];
/** any non-primitive value - can't access any properties (NOT COMMON but useful as placeholder) */
obj2: object;
/** an interface with no required properties - (NOT COMMON, except for things like `React.Component<{}, State>`) */
obj3: {};
/** a dict object with any number of properties of the same type */
dict1: {
[key: string]: MyTypeHere;
};
dict2: Record<string, MyTypeHere>; // equivalent to dict1
/** function that doesn't take or return anything (VERY COMMON) */
onClick: () => void;
/** function with named prop (VERY COMMON) */
onChange: (id: number) => void;
/** function type syntax that takes an event (VERY COMMON) */
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
/** alternative function type syntax that takes an event (VERY COMMON) */
onClick(event: React.MouseEvent<HTMLButtonElement>): void;
/** any function as long as you don't invoke it (not recommended) */
onSomething: Function;
/** an optional prop (VERY COMMON!) */
optional?: OptionalType;
/** when passing down the state setter function returned by `useState` to a child component. `number` is an example, swap out with whatever the type of your state */
setState: React.Dispatch<React.SetStateAction<number>>;
};
Common Types in React
DOM Events Types
When working with DOM events in React, the type of the event can often be inferred from the event handler. However, when you want to extract a function to be passed to an event handler, you will need to explicitly set the type of the event.
Basic example
export default function Form() {
const [value, setValue] = useState("Change me");
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setValue(event.currentTarget.value);
}
return (
<>
<input value={value} onChange={handleChange} />
<p>Value: {value}</p>
</>
);
}
form elements and onChange event types
All the form elements events are the type React.ChangeEvent<T>
, where T
is the HTML Element type. Here’s an example of all the different HTML types:
- For
<input type="text">
the event type isReact.ChangeEvent<HTMLInputElement>
- For
<textarea>
isReact.ChangeEvent<HTMLTextAreaElement>
- For
<select>
we useReact.ChangeEvent<HTMLInputSelect>
How to know the type of the event
It’s not always clear what type you should give to the e inside your onChange
function. This can happen with onClick
, onSubmit
, or any of the other event handlers that DOM elements receive.
Here are some solution:
- In VS Code, hover over the type of the thing you’re trying to pass in, you can see, this outputs to show the type.
- Create an inline function inside
onChange
:<input onChange={(e) => {}} />
; Now you have access toe
, you can hover over it and get the correct event type. - Using
React.ComponentProps
like this:const onChange: React.ComponentProps<"input">["onChange"] = (e) => { console.log(e); }; <input onChange={onChange} />;
- Using type helper called
EventFor
Children
There are two common paths to describing the children of a component. The first is to use the React.ReactNode
type, which is a union of all the possible types that can be passed as children in JSX:
interface ModalRendererProps {
title: string;
children: React.ReactNode;
}
This is a very broad definition of children. The second is to use the React.ReactElement
type, which is only JSX elements and not JavaScript primitives like strings or numbers:
interface ModalRendererProps {
title: string;
children: React.ReactElement;
}
Note, that you cannot use TypeScript to describe that the children are a certain type of JSX elements, so you cannot use the type-system to describe a component which only accepts
<li>
children.
Style Props
When using inline styles in React, you can use React.CSSProperties
to describe the object passed to the style prop. This type is a union of all the possible CSS properties, and is a good way to ensure you are passing valid CSS properties to the style prop, and to get auto-complete in your editor.
interface MyComponentProps {
style: React.CSSProperties;
}
React ComponentProps
When you component wrap basic elements, like div, button, you can use React.ComponentProps
accept all the props for that elements.
Basic usage
import { ComponentProps } from "react";
type MyDivProps = ComponentProps<"div"> & {
myProp: string;
};
const MyDiv = ({ myProp, ...props }: MyDivProps) => {
console.log(myProp!);
return <div {...props} />;
};
extract props from existing components
You can also use it to extract props from existing components.
const SubmitButton = (props: { onClick: () => void }) => {
return <button onClick={props.onClick}>Submit</button>;
};
type SubmitButtonProps = ComponentProps<
typeof SubmitButton
>;
This is especially useful for extracting the props from components you don’t control, perhaps from third-party libraries.
import { ComponentProps } from "react";
import { Button } from "some-external-library";
type MyButtonProps = ComponentProps<typeof Button>;
Get the Props of an Element with the Associated Ref
Refs in React let you access and interact with the properties of an element. Often, it’s used with form elements like inputs and buttons to extract their values or set their properties. The ComponentPropsWithRef
does exactly what it says - provide the component props with its associated ref.
type InputProps = ComponentPropsWithRef<"input">;
In the example above, the InputProps type extracts the props of the input element, including the associated ref.
Implement Types
The more type information provided to TypeScript, the more powerful its type checking is.
Naming Conventions
- Use
PascalCase
for type names. - Do not use the
I
prefix for interfaces. (Something that was copied from statically typed languages) - Use
_
prefix for private properties. - Use consistent naming for component props types (For example, type CustomComponentProps)
Where To Put Your Types in Application Code
- Rule 1: When a type is used in only one place, put it in the same file where it’s used.
interface Props { foo: string bar: number } export const MyComponent = (props: Props) => { // ... }
And when types are truly single-use, You can even inline them:
export const MyComponent = (props: {foo: string; bar: number}) => { // ... }
- Rule 2: Types that are used in more than one place should be moved to a shared location.
|- src |- components |- MyComponent.tsx |- shared.types.ts
If they’re only used in the components folder, I’ll put them there:
|- src |- components- |- MyComponent.tsx |- components.types.ts
In other words, I share the type across the smallest number of modules that need it.
- Rule 3: Types that are used in more than one package in a monorepo should be moved to a shared package. if you’re working on a monorepo with multiple packages? In that case, you should move shared types to a shared package.
|- apps |- app |- website |- docs |- packages |- types |- src |- shared.types.ts
export import types
types.ts file:
export type Launch = {
id: string;
full_name: string;
};
you can use import
or import type
:
import type {Launch} from './types';
Namespaces
As your project size increases, so will the number of types. There is a good chance that there will be name collisions. Namespaces are the solution to this problem. A namespace will not only avoid multiple type declarations but also provide an organisational structure to your projects. Using namespaces effectively can make your codebase clean.
@types
@types
is a special directory in typescript. It is DefintelyTyped which is maintained by TypeScript. DefinitelyTyped try to conclude all the types used in TypeScript, including build-in libraryies and third libraryies.
You can find @types
dictionary in React project node_modules
. The declaration file (*.d.ts files, e.g. index.d.ts) are auto recognised by your project’s tsconfig
as the root types files. The types defined in these files can be used as global types in your project.
For example @types/react/index.d.ts
, which difine type for React project.
Generics
Definiton: Generics take type(s) as input and use them to derive the type of variables or functions.
Purpose: Generics type give you chance to describe the type relationship between input and output.
function identity<T>(arg: T): T {
return arg;
}
/* Explicitly*/
identity<string>("Hello");
/* Implicitly */
identity("World")
type ButtonProp<T> = {
count: T;
countHistory: T[];
}
function Button<T> ({count, countHistory}: ButtonProp<T>) {
return <button>Click Me</button>;
}
Use generics to make your code reusable without writing multiple type definitions.
The unknown
Type
- The
any
type is container type, butunknown
type is not. - All assignments to the
unknown
variable are considered type-correct. But theunknown
type is only assignable to theany
type and theunknown
type itself. - You can narrow
unknown
type and use them.
Narrowing the unknown
Type
You need to narrow the unknown
Type first before using it. For example using:
-
typeof
operators -
instanceof
operators - custom type guard function
- third lib schema, like Zod.
Custom type guard function Example :
function isNumberArray(value: unknown): value is number[] {
return (
Array.isArray(value) && value.every(element => typeof element === "number")
);
}
const unknownValue: unknown = [15, 23, 8, 4, 42, 16];
if (isNumberArray(unknownValue)) {
// Within this branch, `unknownValue` has type `number[]`,
// so we can spread the numbers as arguments to `Math.max`
const max = Math.max(...unknownValue);
console.log(max);
}
Zod example
import { z } from "zod";
// creating a schema for strings
const mySchema = z.string();
// parsing
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError
// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
Using Type Assertions with unknown
If you want to force the compiler to trust you that a value of type unknown
is of a given type, you can use a type assertion like this:
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"
unknown
in Union and Intersection Types
In union, any of the constituent types is unknown
, the union type evaluates to unknown
, except any
. In intersection, intersecting any type with unknown doesn’t change the resulting type.
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
type UnionType5 = unknown | any; // any
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
use unknown
type in fetch
The data
is any
type when fetching!
- Using Zod:
useEffect(()=>{ fetch("http://jsonplaceholder.typicode.com/todos/1") .then((response)=>response.json()) .then((data: unknown)=>{ // use Zod // for example: const todo = todoSchema.parse(data); // ... }); },[]);
- Using ts-reset:
.json
(in fetch) andJSON.parse
both returnunknown
.
JSX.Element vs ReactNode vs ReactElement
A ReactElement
is an object with type, props, and key properties:
interface ReactElement<
P = any,
T extends
| string
| JSXElementConstructor<any> = string
| JSXElementConstructor<any>,
> {
type: T;
props: P;
key: string | null;
}
A JSX.Element
is a ReactElement<any, any>
. It exists as various libraries can implement JSX in their own way:
declare global {
// …
namespace JSX {
// …
interface Element extends React.ReactElement<any, any> {}
// …
}
// …
}
A ReactPortal is a ReactElement
with a children property:
interface ReactPortal extends ReactElement {
children: ReactNode;
}
A ReactNode is a ReactElement
, string
, number
, Iterable<ReactNode>
, ReactPortal
, boolean
, null
, or undefined
:
type ReactNode =
| ReactElement
| string
| number
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined;
Example:
<div> // <- ReactElement
<Component> // <- ReactElement
{condition && 'text'} // <- ReactNode
</Component>
</div>
Due to historical reasons, render
methods of class components return ReactNode
, but function components return ReactElement
.
Hooks and TypeScript
The type definitions from @types/react
include types for the built-in Hooks, so you can use them in your components without any additional setup. They are built to take into account the code you write in your component, so you will get inferred types a lot of the time.
useState
Usually, you can just use infer types:
// Infer the type as "boolean"
const [enabled, setEnabled] = useState(false);
But you still can do it explicitly, but not necessary:
const [enabled, setEnabled] = useState<boolean>(false);
For object type, usually implement like this:
// ...
const [user, serUser] = useState<User | null> (null);
// ...
const name = user?.name;
useRef
const ref = useRef<HTMLElement | null> (null);
return <button ref={ref}>Click Me</button>
useReducer
Exampel:
import {useReducer} from 'react';
interface State {
count: number
};
type CounterAction =
| { type: "reset" }
| { type: "setCount"; value: State["count"] };
const initialState: State = { count: 0 };
function stateReducer(state: State, action: CounterAction): State {
switch (action.type) {
case "reset":
return initialState;
case "setCount":
return { ...state, count: action.value };
default:
throw new Error("Unknown action");
}
}
export default function App() {
const [state, dispatch] = useReducer(stateReducer, initialState);
const addFive = () => dispatch({ type: "setCount", value: state.count + 5 });
const reset = () => dispatch({ type: "reset" });
return (
<div>
<h1>Welcome to my counter</h1>
<p>Count: {state.count}</p>
<button onClick={addFive}>Add 5</button>
<button onClick={reset}>Reset</button>
</div>
);
}
We are using TypeScript in a few key places:
-
interface State
describes the shape of the reducer’s state. -
type CounterAction
describes the different actions which can be dispatched to the reducer. -
const initialState: State
provides a type for the initial state, and also the type which is used by useReducer by default. -
stateReducer(state: State, action: CounterAction): State
sets the types for the reducer function’s arguments and return value.
useContext
The useContext Hook is a technique for passing data down the component tree without having to pass props through components. It is used by creating a provider component and often by creating a Hook to consume the value in a child component.
import { createContext, useContext, useState } from 'react';
type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme|null>(null);
const useGetTheme = () => useContext(ThemeContext);
export default function MyApp() {
const [theme, setTheme] = useState<Theme>('light');
return (
<ThemeContext.Provider value={theme}>
<MyComponent />
</ThemeContext.Provider>
)
}
function MyComponent() {
const theme = useGetTheme();
if (!theme) { throw new Error("theme must be used within a Provider") }
return (
<div>
<p>Current theme: {theme}</p>
</div>
)
}
useMemo
useCallback
FAQ
- When visiting third party API, it return a BIG object. How to name that big object type?