React TypeScript Cheatsheet

repository·main·Indexed 13 days ago

https://github.com/typescript-cheatsheets/react

A comprehensive resource of best practices, code examples, and type definitions for using React with TypeScript. Covers basic application setup, advanced generic types for library authorship, and specific guidance for React 19 features like useImperativeHandle and the removal of defaultProps in function components.

Tokens
33.2K
Snippets
102
Records
113
Agent score
95%

What's inside React TypeScript Cheatsheet

  1. Overview of React TypeScript Cheatsheet

    main

    The React TypeScript Cheatsheet is a resource designed to help developers use React with TypeScript effectively. It provides opinionated best practices and copy-pasteable examples covering a wide range of scenarios, from basic setup to advanced library development.

    Key features include:

    • Basic TS Types and Setup: Guidance for standard React application development.
    • Advanced Generic Types: For developers building reusable type utilities or React+TS libraries.
    • DefinitelyTyped Advice: Guidance for contributing type definitions to the DefinitelyTyped repository.
  2. Avoid null checks using type assertions

    main

    If you prefer not to use custom hooks with runtime checks, you can use TypeScript assertions to bypass null checks. Note that these are less safe than runtime checks because they hide errors that would otherwise be caught during execution.

    Common patterns include:

    • Non-null assertion operator (!): Use contextValue!.property when consuming.
    • Casting an empty object: createContext<T>({} as T).
    • Casting null: createContext<T>(null!).

    Recommendation: Prefer runtime checking and throwing errors over type assertion to ensure better debugging visibility.

    // Option 1: Non-null assertion during consumption
    const currentUser = use(CurrentUserContext);
    const name = currentUser!.username;
    
    // Option 2: Casting empty object as default
    const CurrentUserContext = createContext<CurrentUserContextType>(
      {} as CurrentUserContextType,
    );
    
    // Option 3: Casting null as non-null default
    const CurrentUserContext = createContext<CurrentUserContextType>(null!);
  3. Avoid using empty interfaces, {}, or Object for non-nullish values

    main

    In TypeScript, an empty interface {}, the type {}, and the Object type all represent "any non-nullish value" rather than an empty object. This means they will accept numbers, strings, functions, and objects, but will reject null or undefined. Using these types is a common source of misunderstanding and is generally not recommended.

    interface AnyNonNullishValue {} // equivalent to `type AnyNonNullishValue = {}` or `type AnyNonNullishValue = Object`
    
    let value: AnyNonNullishValue;
    
    value = 1;           // fine
    value = "foo";      // fine
    value = () => {};   // fine
    value = {};         // fine
    value = { a: 1 };   // fine
    
    value = undefined;  // Error
    value = null;       // Error
  4. Understand the `ReactNode` type

    main

    ReactNode is the broadest type in React, representing anything that can be rendered by React. It is a union of all valid child values, including:

    • ReactElement (the result of JSX, createElement, or cloneElement)
    • string
    • number
    • bigint
    • boolean (true and false render as nothing)
    • null
    • undefined
    • Iterable<ReactNode> (arrays or other iterables of nodes)
    • ReactPortal
    • Promise<ReactNode> (used for async Server Components, unwrapped via <Suspense>)
  5. Allow passing all props or nothing at all

    main

    When you want a component to accept either a complete set of props or no props at all, you have two main approaches:

    1. Grouping props in an optional object: Wrap the required props in a single optional property. This ensures that if the property is provided, all its internal fields must be present.
    2. Using Record<string, never>: This represents an empty object, but it is not officially recommended by the TypeScript team.

    Grouping in an optional object is the cleaner, more idi-omatic approach.

    // Recommended: Grouping required props in an optional object
    interface Props {
      obj?: {
        a: string;
        b: string;
      };
    }
    
    const AllOrNothing = (props: Props) => {
      if (props.obj) {
        return <>{props.obj.a}</>;
      }
      return <>Nothing</>;
    };
    
    const Component = () => (
      <>
        <AllOrNothing /> {/* ok */}
        <AllOrNothing obj={{ a: "", b: "" }} /> {/* ok */}
        <AllOrNothing obj={{ a: "" }} /> {/* error */}
      </>
    );
  6. Use Concurrent React APIs: Suspense, use, and Transitions

    main

    Concurrent React APIs allow you to keep the UI responsive during heavy rendering or data fetching.

    Suspense

    Declaratively show a fallback UI while children are loading (e.g., waiting for a promise via use or a lazy component).

    use

    Reads the value of a context or a promise. Unlike useContext, use can be called inside conditions and loops and integrates with Suspense for promises.

    useTransition

    Marks a state update as non-urgent. This prevents heavy renders from blocking urgent interactions like typing or scrolling.

    • Async Transitions (React 19): The function passed to startTransition can be async. isPending will remain true until the entire async operation completes.

    useDeferredValue

    Defers re-rendering a part of the UI that is expensive to compute. The deferred value lags behind the actual value, allowing urgent updates to flush first.

    • initialValue (React 19): Accepts a second argument to provide a value to use during the initial render before the deferred value catches up.
    // Suspense + use
    const UserProfile = ({ userPromise }: { userPromise: Promise<User> }) => {
      const user = use(userPromise);
      return <p>{user.name}</p>;
    };
    
    // useTransition
    const [isPending, startTransition] = useTransition();
    const selectTab = (next: string) => {
      startTransition(() => {
        setTab(next);
      });
    };
    
    // useDeferredValue
    const deferredQuery = useDeferredValue(query, "");
  7. Use Discriminated Unions for expressive component APIs

    main

    Discriminated Unions allow you to create component props where the availability of certain properties depends on a specific 'discriminant' key. This is highly effective for complex state or event handling.

    Warning: TypeScript does not narrow Discriminated Unions based on typeof checks of the values. You must check the value of the discriminant key itself (e.g., if (event.type === 'TextEvent')) rather than checking the type of a property (e.g., if (typeof event.value === 'string')).

    type UserTextEvent = { type: "TextEvent"; value: string; target: HTMLInputElement };
    type UserMouseEvent = { type: "MouseEvent"; value: [number, number]; target: HTMLElement };
    type UserEvent = UserTextEvent | UserMouseEvent;
    
    function handle(event: UserEvent) {
      if (event.type === "TextEvent") {
        event.value; // string
        event.target; // HTMLInputElement
        return;
      }
      event.value; // [number, number]
      event.target; // HTMLElement
    }
  8. When to use React.FC vs. Normal Functions

    main

    While React.FC (or React.FunctionComponent) is common in older codebases, the current consensus for modern React (React 18+) and TypeScript (5.1+) is that it is often unnecessary.

    Differences to consider:

    • Return Type: React.FC is explicit about the return type, whereas normal functions rely on inference unless manually annotated.
    • Static Properties: React.FC provides typechecking and autocomplete for static properties like displayName, propTypes, and defaultProps.
    • Readonly Props: In the future, React.FC may automatically mark props as readonly, though this is less relevant if you destructure props in the parameter list.

    Recommendation: Use normal functions with explicit prop types for simplicity. If you are on React 17 or TypeScript < 5.1, using React.FC is generally discouraged.

  9. Handle numbers vs strings in `CSSProperties`

    main

    When using CSSProperties, React handles numeric values differently depending on the property:

    • Length-like properties: If you provide a number, React automatically appends px (e.g., width: 100 becomes width: 100px). Use string for other units like %, rem, or em.
    • Unitless properties: Properties like lineHeight, opacity, zIndex, and flexGrow accept number without appending units.
    <div style={{ width: 100 }} />       // → width: 100px
    <div style={{ width: "100%" }} />    // → width: 100%
    <div style={{ width: "10rem" }} />   // → width: 10rem
  10. Allow one or the other prop but not both

    main

    To enforce that a component accepts either propA or propB, but never both simultaneously, you can use a union of types where the unwanted prop is explicitly typed as never.

    Alternatively, a more robust pattern is using a discriminated union with a type prop. This is often preferred as it provides clearer error messages and easier type narrowing within the component logic.

    // Pattern 1: Using `never` to disallow both
    type Props1 = { foo: string; bar?: never };
    type Props2 = { bar: string; foo?: never };
    const OneOrTheOther = (props: Props1 | Props2) => {
      if ("foo" in props && typeof props.foo === "string") {
        return <>{props.foo}</>;
      }
      return <>{props.bar}</>;
    };
    
    // Pattern 2: Using a discriminant prop (Recommended)
    type Props1 = { type: "foo"; foo: string };
    type Props2 = { type: "bar"; bar: string };
    
    const OneOrTheOther = (props: Props1 | Props2) => {
      if (props.type === "foo") {
        return <>{props.foo}</>;
      }
      return <>{props.bar}</>;
    };
  11. Use Option and Maybe patterns for safer error handling

    main

    Instead of traditional error handling, you can use functional patterns like Option (also known as Maybe) to represent values that might be absent. This involves using an interface with methods like flatMap and getOrElse to chain operations safely without explicit null or error checks at every step.

    interface Option<T> {
      flatMap<U>(f: (value: T) => None): None;
      flatMap<U>(f: (value: T) => Option<U>): Option<U>; // Note: simplified for example
      getOrElse(value: T): T;
    }
    
    class Some<T> implements Option<T> {
      constructor(private value: T) {}
      flatMap<U>(f: (value: T) => Option<U>): Option<U> {
        return f(this.value);
      }
      getOrElse(): T {
        return this.value;
      }
    }
    
    class None implements Option<never> {
      flatMap<U>(): None {
        return this;
      }
      getOrElse<U>(value: U): U {
        return value;
      }
    }
    
    // Usage:
    let result = Option(6)
      .flatMap((n) => Option(n * 3))
      .getOrElse(7);
  12. Type the structure of React children

    main

    You can enforce the structure of children in your component props, such as requiring a specific number of children or a specific type of array.

    Note on Limitations: You cannot specify which specific components are allowed as children (e.g., enforcing that <Routes> only accepts <Route> as children) because JSX expressions are blackboxed into a generic React.JSX.Element type by TypeScript.

    type OneChild = React.ReactNode;
    type TwoChildren = [React.ReactNode, React.ReactNode];
    type ArrayOfProps = SomeProp[];
    type NumbersChildren = number[];
    type TwoNumbersChildren = [number, number];