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 and ts files instead of js 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 is React.ChangeEvent<HTMLInputElement>
  • For <textarea> is React.ChangeEvent<HTMLTextAreaElement>
  • For <select> we use React.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 to e, 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, but unknown type is not.
  • All assignments to the unknown variable are considered type-correct. But the unknown type is only assignable to the any type and the unknown 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) and JSON.parse both return unknown.

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?

Reference