Chapter 10. TypeScript and React

React is arguably one of the most popular JavaScript libraries in recent years. Its simple approach to the composition of components has changed the way we write front-end (and to an extent back-end) applications fundamentally, allowing you to declaratively write UI code using a JavaScript syntax extension called JSX. This simple principle was not only easy to pick up and understand, but it also influenced dozens of other libraries to this day.

JSX is undoubtedly a game-changer in the JavaScript world, and with TypeScript’s goal to cater to all JavaScript developers, JSX found its way into TypeScript as well. In fact, TypeScript is a full-fledged JSX compiler. If you have no need for additional bundling or extra tooling, TypeScript is all you need to get your React app going. TypeScript is also immensely popular. At the time of writing, the React typings on NPM clocked in 20 million downloads per week. The fantastic tooling with VS Code and the excellent types made TypeScript the first choice for React developers around the globe.

While TypeScript’s popularity among React developers continues unabated, there is one circumstance that makes the usage of TypeScript with React a bit difficult: TypeScript isn’t the React team’s first choice. While other JSX-based libraries are now mostly written in TypeScript and therefore provide excellent types out of the box, the React team works with their own static type checker called Flow, which is similar, but ultimately incompatible with TypeScript. This means that the React types millions of developers rely on are made subsequently by a group of community contributors, and published on Definitely Typed. While @types/react are considered to be excellent, they are still just the best effort to type a library as complex as React. This inevitably leads to gaps, and where those gaps become visible, this chapter will be your guide.

In this chapter, we look at situations where React is supposed to be easy, but TypeScript gives you a hard time throwing complex error messages at you. We are going to figure out what those messages mean, how you can work around them and what solutions help you in the long run. You will also learn about various development patterns and their benefit, and how to use TypeScript’s built-in JSX support for your benefit.

What you won’t get is a basic set-up guide for React and TypeScript. The ecosystem is so vast and rich, there are many ways that lead to Rome. Pick your framework’s documentation pages and look out for TypeScript. Also note that I assume some React experience upfront. In this chapter, we deal mostly about typing React.

While there is naturally a strong inclination towards React in this chapter, you will be able to use certain learnings and apply them to other JSX-based frameworks and libraries as well.

10.1 Writing Proxy Components

Problem

You write a lot of standard HTML components, but you don’t want to set all necessary properties all the time.

Solution

Create proxy components, and apply a few patterns to make them usable for your scenario.

Discussion

There is rarely a web application that doesn’t use buttons. Buttons have a type property which defaults to submit. This is a sensible default for forms where you perform an action over HTTP, where you POST the contents to a server-side API. But when you just want to have interactive elements on your site, the correct type for buttons is button. This is not only an aesthetic choice, but also important for accessibility.

<button type="button">Click me!</button>

When you write React, chances are you rarely submit a form to a server with a submit type, but interact with lots of button-type buttons. A good way to deal with situations like this is to write proxy components. They mimic HTML eleents, but preset already a couple of properties.

function Button(props) {
  return <button type="button" {...props} />;
}

The idea is that Button takes the same properties as the HTML button, and the attributes are spread out to the HTML element. Spreading attributes to HTML elements is a nice feature where you can make sure that you are able to set all the HTML properties that an element has without knowing upfront which you want to set. But how do we type them?

All HTML elements that can be used in JSX are defined through intrinsic elements in the JSX namespace. When you load React, the JSX namespace appears as a global namespace in your file, and you can access all elements via index access. So the correct prop types for Button are defined in JSX.IntrinsicElements.

Note

An alternative to JSX.IntrinsicElements is React.ElementType, a generic type within the React package, which also includes class and function components. For proxy components, JSX.IntrinsicElements is sufficient enough, and comes with an extra benefit: Your components stay compatible to other React-like frameworks like Preact.

JSX.IntrinsicElements is a type within the global JSX namespace. Once this namespace is in scope, TypeScript is able to pick up basic elements which are compatible with your JSX-based framework.

type ButtonProps = JSX.IntrinsicElements["button"];

function Button(props: ButtonProps) {
  return <button type="button" {...props} />;
}

This includes children, we spread them along! As you see, we set a button’s type to button. Since props are just JavaScript objects, it’s possible to override type by setting it as an attribute in props. If two keys with the same name are defined, the last one wins. This might be desired behaviour, but you alternatively may want to prevent you and your colleagues from overriding type. With the Omit<T, K> helper type, you can take all properties from a JSX button, but drop keys you don’t want to override.

type ButtonProps = Omit<JSX.IntrinsicElements["button"], "type">;

function Button(props: ButtonProps) {
  return <button type="button" {...props} />;
}

const aButton = <Button type="button">Hi</Button>;
//                      ^
// Type '{ children: string; type: string; }' is not
// assignable to type 'IntrinsicAttributes & ButtonProps'.
// Property 'type' does not exist on type
// 'IntrinsicAttributes & ButtonProps'.(2322)

If you need type to be submit, you can create another proxy component.

type SubmitButtonProps = Omit<JSX.IntrinsicElements["button"], "type">;

function SubmitButton(props: SubmitButtonProps) {
  return <button type="submit" {...props} />;
}

You can extend this idea of omitting properties if you want to preset even more properties. Maybe you adhere to a design system and don’t want class names to be set arbitrarily.

type StyledButton = Omit<
  JSX.IntrinsicElements["button"],
  "type" | "className" | "style"
> & {
  type: "primary" | "secondary";
};

function StyledButton({ type, ...allProps }: StyledButton) {
  return <Button type="button" className={`btn-${type}`} {...allProps}/>;
}

This even allows you to reuse the type property name.

We dropped some props from the type definition and preset them to sensible defaults. Now we want to make sure our users don’t forget to set some props. Like the alt attribute of an image or the src attribute.

For that, we create a MakeRequired helper type that removes the optional flag.

type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>;

And build our own props:

type ImgProps
  = MakeRequired<
    JSX.IntrinsicElements["img"],
    "alt" | "src"
  >;

export function Img(props: ImgProps) {
  return <img {...props} />;
}

const anImage = <Img />;
//               ^
// Type '{}' is missing the following properties from type
// 'Required<Pick<DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>,
//  HTMLImageElement>, "alt" | "src">>': alt, src (2739)

With just a few changes to the original intrinsic element’s type and a proxy component, we can make sure that our code becomes more robust, more accessible, and less error prone.

10.2 Writing Controlled Components

Problem

Form elements like inputs add another complexity as we need to decide where to manage state: In the browser, or in React.

Solution

Write a proxy component that uses discriminated unions and the optional never technique to ensure you won’t switch from uncontrolled to controlled at runtime.

Discussion

React differentiates form elements between controlled components and uncontrolled components. When you use regular form elements like input, textarea, or select, you need to keep in mind that the underlying HTML elements control their own state. Whereas in React, the state of an element is also defined through React.

If you set the value attribute, React assumes that the element’s value is also controlled by React’s state management, which means that you are not able to modifiy this value unless you maintain the element’s state using useState and the associated setter function.

There are two ways to deal with this. First, you can choose defaultValue as a property instead of value. This will only set the value of the input in the first rendering, and subsequently leaves everything in the hands of the browser.

function Input({
  value = "", ...allProps
}: Props) {
  return (
    <input
      defaultValue={value}
      {...allProps}
    />
  );
}

Or you manage value interally via React’s state management. Usually, it’s enough to just intersect the original input element’s props with our own type. We drop value from the intrinsic elements, and add it as an required string.

type ControlledProps =
  Omit<JSX.IntrinsicElements["input"], "value"> & {
    value: string;
  };

Then, we wrap the input element in a proxy component. It is not best practice to keep state internally in a proxy component, but rather manage it from the outside with useState. We also forward the onChange handler we pass from the original input props.

function Input({
  value = "", onChange, ...allProps
}: ControlledProps) {
  return (
    <input
      value={value}
      {...allProps}
      onChange={onChange}
    />
  );
}

function AComponentUsingInput() {
  const [val, setVal] = useState("");
  return <Input
    value={val}
    onChange={(e) => {
      setVal(e.target.value);
    }}
  />
}

Furthermore, there is an interesting warning react raises when dealing with a switch from uncontrolled to controlled at runtime: Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.

We can prevent this warning by making sure at compile time that we either always provide a defined string value, or that we provide a defaultValue instead. But not both. This can be solved by using a discriminated union type using the optional never technique (as seen in Recipe 3.8), and use the OnlyRequired helper type from Recipe 8.1 to derive possible properties from JSX.IntrinsicElements["input"].

import React, { useState } from "react";

// A helper type setting a few properties to be required
type OnlyRequired<T, K extends keyof T = keyof T> = Required<Pick<T, K>> &
  Partial<Omit<T, K>>;

// Branch 1: Make "value" and "onChange" required, drop `defaultValue`
type ControlledProps = OnlyRequired<
  JSX.IntrinsicElements["input"],
  "value" | "onChange"
> & {
  defaultValue?: never;
};

// Branch 2: Drop `value` and `onChange`, make `defaultValue` required
type UncontrolledProps = Omit<
  JSX.IntrinsicElements["input"],
  "value" | "onChange"
> & {
  defaultValue: string;
  value?: never;
  onChange?: never;
};

type InputProps = ControlledProps | UncontrolledProps;

function Input({ ...allProps }: InputProps) {
  return <input {...allProps} />;
}

function Controlled() {
  const [val, setVal] = useState("");
  return <Input value={val} onChange={(e) => setVal(e.target.value)} />;
}

function Uncontrolled() {
  return <Input defaultValue="Hello" />;
}

All other cases, having an optional value or having a defaultValue and trying to control values, will be prohibited by the type system.

10.3 Typing Custom Hooks

Problem

You want to define custom hooks and get proper types.

Solution

Use tuple types or const context.

Discussion

Let’s create a custom hook in React and stick to the naming convention as regular React hooks do: Returning an array (or tuple) that can be destructured. For example useState:

const [state, setState] = useState(0);

Why do we even use arrays? Because the array’s fields have no name, and you can set names on your own:

const [count, setCount] = useState(0);
const [darkMode, setDarkMode] = useState(true);

So naturally, if you have a similar pattern, you also want to return an array. A custom toggle hook might look like this:

export const useToggle = (initialValue: boolean) => {
  const [value, setValue] = useState(initialValue);
  const toggleValue = () => setValue(!value);
  return [value, toggleValue];
}

Nothing out of the ordinary. The only types we have to set are the types of the input parameters. Let’s try to use it:

export const Body = () => {
  const [isVisible, toggleVisible] = useToggle(false)
  return (
    <>
      <button onClick={toggleVisible}>Hello</button>
	  { /* Error. See below */ }
      {isVisible && <div>World</div>}
    </>
  )
}
// Error: Type 'boolean | (() => void)' is not assignable to
// type 'MouseEventHandler<HTMLButtonElement> | undefined'.
// Type 'boolean' is not assignable to type
// 'MouseEventHandler<HTMLButtonElement>'.(2322)

So why does this fail? The error message might be very cryptic, but what we should look out for is the first type, which is declared incompatible: boolean | (() => void)'. This comes from returning an array. An array is a list of any length, that can hold as many elements as virtually possible. From the return value in useToggle, TypeScript infers an array type. Since the type of value is boolean (great!) and the type of toggleValue is (() => void) (a function expected to return nothing), TypeScript tells us that both types are possible in this array.

And this is what breaks the compatibility with onClick. onClick expects a function. Good, toggleValue (or toggleVisible) is a function. But according to TypeScript, it can also be a boolean! TypeScript tells you to be explicit, or at least do type checks.

But we shouldn’t need to do extra type-checks. Our code is very clear. It’s the types that are wrong. Because we’re not dealing with an array. Let’s go for a different name: Tuple. While an array is a list of values that can be of any length, we know exactly how many values we get in a tuple. Usually, we also know the type of each element in a tuple.

So we shouldn’t return an array, but a tuple at useToggle. The problem: In JavaScript an array and a tuple are indistinguishable. In TypeScript’s type system, we can distinguish them.

First possibility: Let’s be intentional with our return type. Since TypeScript — correctly! — infers an array, we have to tell TypeScript that we are expecting a tuple.

// add a return type here
export const useToggle = (initialValue: boolean): [boolean, () => void] => {
  const [value, setValue] = useState(initialValue);
  const toggleValue = () => setValue(!value);
  return [value, toggleValue];
};

With [boolean, () => void] as a return type, TypeScript checks that we are returning a tuple in this function. TypeScript does not infer anymore, but rather makes sure that your intended return type is matched by the actual values. And voila, your code doesn’t throw errors anymore.

The second option involves const context. With a tuple, we know how many elements we are expecting, and know the type of these elements. This sounds like a job for freezing the type with a const assertion.

export const useToggle = (initialValue: boolean) => {
  const [value, setValue] = useState(initialValue);
  const toggleValue = () => setValue(!value);
  // here, we freeze the array to a tuple
  return [value, toggleValue] as const;
}

The return type is now readonly [boolean, () => void], because as const makes sure that your values are constant, and not changeable. This type is a little bit different semantically, but in reality, you wouldn’t be able to change the values you return outside of useToggle. So being readonly would be slightly more correct.

10.4 Typing Generic forwardRef Components

Problem

You use forwardRef for your components, but you need them to be generic.

Solution

There are several solutions to this problem. Let’s discuss!

Disccussion

If you are creating component libraries and design systems in React, you might already have fowarded Refs to the DOM elements inside your components.

This is especially useful if you wrap basic components or leaves_ in proxy components (see Recipe 10.1, but want to use the ref property just like you’re used to:

const Button = React.forwardRef((props, ref) => (
  <button type="button" {...props} ref={ref}>
    {props.children}
  </button>
));

// Usage: You can use your proxy just like you use
// a regular button!
const reference = React.createRef();
<Button className="primary" ref={reference}>Hello</Button>

Providing types for React.forwardRef is usually pretty straightforward. The types shipped by @types/react have generic type variables that you can set upon calling React.forwardRef. In that case, explicitly annotating your types is the way to go!

type ButtonProps = JSX.IntrinsicElements["button"];

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => (
    <button type="button" {...props} ref={ref}>
      {props.children}
    </button>
  )
);

// Usage
const reference = React.createRef<HTMLButtonElement>();
<Button className="primary" ref={reference}>Hello</Button>

So far, so good. But things get a bit hairy if you have a component that accepts generic properties. The following component that produces a list of list items, where you can select each row with a button element:

type ClickableListProps<T> = {
  items: T[];
  onSelect: (item: T) => void;
};

function ClickableList<T>(props: ClickableListProps<T>) {
  return (
    <ul>
      {props.items.map((item, idx) => (
        <li>
          <button key={idx} onClick={() => props.onSelect(item)}>
            Choose
          </button>
          {item}
        </li>
      ))}
    </ul>
  );
}

// Usage
const items = [1, 2, 3, 4];
<ClickableList items={items}
  onSelect={(item) => {
    // item is of type number
    console.log(item);
  } } />

You want the extra type-safety so you can work with a type-safe item in your onSelect callback. Say you want to create a ref to the inner ul element, how do you proceed? Let’s change the ClickableList component to an inner function component that takes a ForwardRef, and use it as an argument in the React.forwardRef function.

// The original component extended with a `ref`
function ClickableListInner<T>(
  props: ClickableListProps<T>,
  ref: React.ForwardedRef<HTMLUListElement>
) {
  return (
    <ul ref={ref}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}

// As an argument in `React.forwardRef`
const ClickableList = React.forwardRef(ClickableListInner)

This compiles, but has one downside: We can’t assign a generic type variable for ClickableListProps. It becomes unknown by default. Which is good compared to any, but also slightly annoying. When we use ClickableList, we know which items to pass along! We want to have them typed accordingly! So how can we achieve this? The answer is tricky…​ and you have a couple of options.

The first option is to do a type assertion that restores the original function signature.

const ClickableList = React.forwardRef(ClickableListInner) as <T>(
  props: ClickableListProps<T> & { ref?: React.ForwardedRef<HTMLUListElement> }
) => ReturnType<typeof ClickableListInner>;

Type assertions work great if you happen to have just a few situations where you need to have generic forwardRef components, but might be too clumsy when you work with lots of them. Also, you introduce an unsafe operator for something that should be default behaviour.

The second option is to create custom references with warpper components. While ref is a reserved word for React components, you can use your own, custom props to mimic a similar behavior. This works just as well.

type ClickableListProps<T> = {
  items: T[];
  onSelect: (item: T) => void;
  mRef?: React.Ref<HTMLUListElement> | null;
};

export function ClickableList<T>(
  props: ClickableListProps<T>
) {
  return (
    <ul ref={props.mRef}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}

You introduce a new API, though. For the record, there is also the possibility of using a wrapper component, that allows you to use forwardRef inside in an inner component and expose a custom ref property to the outside.

function ClickableListInner<T>(
  props: ClickableListProps<T>,
  ref: React.ForwardedRef<HTMLUListElement>
) {
  return (
    <ul ref={ref}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}

const ClickableListWithRef = forwardRef(ClickableListInner);

type ClickableListWithRefProps<T> = ClickableListProps<T> & {
  mRef?: React.Ref<HTMLUListElement>;
};

export function ClickableList<T>({
  mRef,
  ...props
}: ClickableListWithRefProps<T>) {
  return <ClickableListWithRef ref={mRef} {...props} />;
}

Both are valid solutions if the only thing you want to achieve is passing that ref. If you want to have a consistent API, you might look for something else.

The last option is to augment forwardRef with your own type definitions. TypeScript has a feature called higher-order function type inference, that allows propagating free type parameters on to the outer function.

This sounds a lot like what we want to have with forwardRef to begin with, but for some reason it doesn’t work with our current typings. The reason is that higher-order function type inference only works on plain function types. the function declarations inside forwardRef also add properties for defaultProps, etc. Relics from the class component days. Things you might not want to use anyway.

So without the additional properties, it should be possible to use higher-order function type inference!

We are using TypeScript, we have the possibility to redeclare and redefine global module, namespace and interface declarations on our own. Declaration merging is a powerful tool, and we’re going to make use of it.

// Redecalare forwardRef
declare module "react" {
  function forwardRef<T, P = {}>(
    render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
  ): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}


// Just write your components like you're used to!

type ClickableListProps<T> = {
  items: T[];
  onSelect: (item: T) => void;
};
function ClickableListInner<T>(
  props: ClickableListProps<T>,
  ref: React.ForwardedRef<HTMLUListElement>
) {
  return (
    <ul ref={ref}>
      {props.items.map((item, i) => (
        <li key={i}>
          <button onClick={(el) => props.onSelect(item)}>Select</button>
          {item}
        </li>
      ))}
    </ul>
  );
}

export const ClickableList = React.forwardRef(ClickableListInner);

The nice thing about this solution is that you write regular JavaScript again, and work exclusively on a type level. Also, redeclarations are module-scoped. No interference with any forwardRef calls from other modules!

10.5 Providing Types for the Context API

Problem

You want to use the context API for globals in your app, but you don’t know how to best deal with type definitions.

Solution

Either set default properties for context and let the type be inferred, or create a partial of your context’s properties and instantiate the generic type parameter explicitly. If you don’t want to provide default values, but want to make sure that all properties are provided, create a helper function.

Discussion

React’s context API allows you to share data on a global level. To use it, you need two things:

  • A provider. Providers pass data to a subtree.

  • Consumers. Consumers are components that consume the passed data inside render props

With React’s typings, you can use context without doing anything else most of the time. Everything is done using type inference and generics.

Let’s have a look! First, we create a context. Here, we want to store global application settings, like a theme, the app’s language along with the global state. When creating a React context, we want to pass default properties.

import React from "react";

const AppContext = React.createContext({
  authenticated: true,
  lang: "en",
  theme: "dark",
});

And with that, everything you need to do in terms of types is done for you. We have three properties called authenticated, lang, and theme, they are of types boolean and string respectively. React’s typings take this information to provide you with the correct types when you use them.

Next, a component high up in your component tree needs to provide context, for example, the application’s root component. This provider trickles down the values you’ve set to every consumer below.

function App() {
  return (
    <AppContext.Provider
      value={{
        authenticated: true,
        lang: "de",
        theme: "light",
      }}
    >
      <Header />
    </AppContext.Provider>
  );
}

Now, every component inside this tree can consume this context. You already get type errors when you forget a property or use the wrong type:

function App() {
// Property 'theme' is missing in type '{ lang: string; }' but required
// in type '{ lang: string; theme: string; authenticated: boolean }'.(2741)
  return (
    <AppContext.Provider
      value={{
        lang: "de",
      }}
    >
      <Header />
    </AppContext.Provider>
  );
}

Now, let’s consume our global state. Consuming context can be done via render props. You can destructure your render props as deep as you like, to get only the props you want to deal with:

function Header() {
  return (
    <AppContext.Consumer>
      {({ authenticated }) => {
        if (authenticated) {
          return <h1>Logged in!</h1>;
        }
        return <h1>You need to sign in</h1>;
      }}
    </AppContext.Consumer>
  );
}

Another way of using context is via the respective useContext hook.

function Header() {
  const { authenticated } = useContext(AppContext);
  if (authenticated) {
    return <h1>Logged in!</h1>;
  }
  return <h1>You need to sign in</h1>;
}

Because we defined our properties earlier with the right types, authenticated is of type boolean at this point. Again we didn’t have to do anything to get this extra type safety.

The whole example above works best if we have default properties and values. Sometimes, you don’t have default values or you need to be more flexible in which properties you want to set.

Instead of inferring everything from default values, we annotate the generic type parameter explicitly, but not with the full type, but with a Partial.

We create a type for the context’s props.

type ContextProps = {
  authenticated: boolean;
  lang: string;
  theme: string;
};

And initialize the new context.

const AppContext = React.createContext<Partial<ContextProps>>({});

Changing the semantics of the context’s default properties has some side effects on your components as well. Now you don’t need to provide every value, an empty context object can just do the same! All your properties are optional.

function App() {
  return (
    <AppContext.Provider
      value={{
        authenticated: true,
      }}
    >
      <Header />
    </AppContext.Provider>
  );
}

This also means that you need to check for every property if it’s defined. This doesn’t change the code where you rely on boolean values, but every other property needs to have another undefined check.

function Header() {
  const { authenticated, lang } = useContext(AppContext);
  if (authenticated && lang) {
    return <>
      <h1>Logged in!</h1>
      <p>Your language setting is set to {lang}</p>
    </>;
  }
  return <h1>You need to sign in (or don't you have a language setting?)</h1>;
}

If you can’t provide default values and want to make sure that all properties are provided by a context provider, we can help ourselves with a little helper function. Here, we want explicit generic instantiation to supply a type, but give the right type guards so that when consuming context, all possibly undefined values are correctly set.

function createContext<Props extends {}>() { // (1)
  const ctx = React.createContext<Props | undefined>(undefined); // (2)
  function useInnerCtx() { // (3)
    const c = useContext(ctx);
    if (c === undefined) // (4)
      throw new Error("Context must be consumed within a Provider");
    return c; // (5)
  }
  return [useInnerCtx, ctx.Provider as React.Provider<Props>] as const; // (6)
}

What’s going on in createContext?

  1. We create a function that has no function arguments, but generic type parameters. Without the connection to function parameters, we can’t instantiate Props via inference. This means that for createContext to provide proper types, we need to explicitly instantiate it.

  2. We create a context that allows for Props or undefined. With undefined added to the type, we can pass undefined as value. No default values!

  3. Inside createContext, we create a custom hook. This hook wraps useContext using the newly created context ctx.

  4. Then we do a type guard where we check if the returned Props include undefined. Remember, when calling createContext, we instantiate the generic type parameter with Props | undefined. This line removes undefined from the union type again.

  5. Which means that here, c is Props.

  6. We assert that ctx.Provider doesn’t take undefined values. We call as const to return [useInnerContext, ctx.Provider] as a tuple type.

Use createContext similar to React.createContext.

const [useAppContext, AppContextProvider] = createContext<ContextProps>();

When using AppContextProvider, we need to provide all values.

function App() {
  return (
    <AppContextProvider
      value={{ lang: "en", theme: "dark", authenticated: true }}
    >
      <Header />
    </AppContextProvider>
  );
}

function Header() {
  // consuming Context doesn't change much
  const { authenticated } = useAppContext();
  if (authenticated) {
    return <h1>Logged in!</h1>;
  }
  return <h1>You need to sign in</h1>;
}

Depending on your use case you have exact types without too much overhead for you.

10.6 Typing Higher Order Components

Problem

You are writing higher-order components to preset certain properties for other components, but don’t know how to type them.

Solution

Use the React.ComponentType<P> type from @types/react to define a component that extends upon your preset attributes.

Discussion

React is influenced by functional programming, which we see in the way components are designed (via functions), assembled (via composition), and updated (stateless, uni-directional data flow). It didn’t take long for functional programming techniques and paradigms to find their way into React development. One such technique is higher-order components, which draw inspiration from higher-order functions.

Higher-order functions accept one or more parameters to return a new function. Sometimes those parameters are here to prefill certain other parameters, as we see in e.g. all currying recipes from Chapter 7. Higher order components are similar: They take one or more components and return themselves another component. Usually, you create them to prefill certain properties where you want to make sure that they won’t be changed later on.

Let’s think about a general-purpose Card component, which takes title and content as strings.

type CardProps = {
  title: string;
  content: string;
};

function Card({ title, content }: CardProps) {
  return (
    <>
      <h2>{title}</h2>
      <div>{content}</div>
    </>
  );
}

You use this card to present certain events, like warnings, information bubbles, and error messages. The most basic information card has "Info" as its title.

<Card title="Info" content="Your task has been processed" />;

You could subset the properties of Card to only allow for a certain subset of strings for title, but on the other hand, you want to be able to reuse Card as much as possible. You rather create a new component that already sets title to "Info", and only allows for other properties to be set.

const Info = withInjectedProps({ title: "Info" }, Card);

// This should work
<Info content="Your task has been processed" />;

// This should throw an error
<Info content="Your task has been processed" title="Warning" />;

In other words, you inject a subset of properties already, and only set the remaining ones with the newly created component. A function withInjectedProps is easily written.

function withInjectedProps(injected, Component) {
  return function (props) {
    const newProps = { ...injected, ...props };
    return <Component {...newProps} />;
  };
}

It takes the injected props and a Component as parameters, returns a new function component that takes the remaining props as parameters, and instantiates the original component with merged properties.

So how do we type withInjectedProps? Let’s look at the result, and see what’s inside.

function withInjectedProps<T extends {}, U extends T>( // (1)
  injected: T,
  Component: React.ComponentType<U> // (2)
) {
  return function (props: Omit<U, keyof T>) { // (3)
    const newProps = { ...injected, ...props } as U; // (4)
    return <Component {...newProps} />;
  };
}

This is what’s going on:

  1. We need to define two generic type parameters. T for the props we already inject, it extends from {} to make sure we only pass objects. U is a generic type parameter for all props of Component. U extends T, which means that U is a subset of T. This says that U has more properties than T, but needs to include what T already has defined.

  2. We define Component to be of type React.ComponentType<U>. This includes class components as well as function components, and says that props will be set to U. With the relationship of T and U and the way we defined the parameters of withInjectedProps, we make sure that everything that will be passed for Component defines a subset of properties for Component with injected. If we have a typo, we already get the first error message!

  3. The function component that will be returned takes the remaining props. With Omit<U, keyof T> we make sure that we don’t allow prefilled attributes to be set again.

  4. Merging T and Omit<U, keyof T> should result in U again, but since generic type parameters can be explicitly instantiated with something different, they might not fit Component again. A little type assertion helps us to make sure that the props are actually what we want.

And that’s it! With those new types, we get proper auto-complete and errors:

const Info = withInjectedProps({ title: "Info" }, Card);

<Info content="Your task has been processed" />;
<Info content="Your task has been processed" title="Warning" />;
//                                           ^
// Type '{ content: string; title: string; }' is not assignable
// to type 'IntrinsicAttributes & Omit<CardProps, "title">'.
// Property 'title' does not exist on type
// 'IntrinsicAttributes & Omit<CardProps, "title">'.(2322)

withInjectedProps is so flexible, that we can derive higher order functions that create higher order components for various situations, like withTitle, which is just here to prefill title attributes of type string.

function withTitle<U extends { title: string }>(
  title: string,
  Component: React.ComponentType<U>
) {
  return withInjectedProps({ title }, Component);
}

Your functional programming goodness knows no boundaries.

10.7 Typing Callbacks in React’s Synthetic Event System

Problem

You want to get the best possible typings for all browser events in React, and use the type system to restrict your callbacks to compatible elements.

Solution

Use the event types of @types/react and specialize on components using generic type parameters.

Discussion

Web applications live by interacting with them. Events are key, and TypeScript’s React typings have great support for events, but require you not to use the native events from lib.dom.d.ts. If you do, React throws errors at you:

type WithChildren<T = {}> = T & { children?: React.ReactNode };

type ButtonProps = {
  onClick: (event: MouseEvent) => void;
} & WithChildren;

function Button({ onClick, children }: ButtonProps) {
  return <button onClick={onClick}>{children}</button>;
//               ^
// Type '(event: MouseEvent) => void' is not assignable to
// type 'MouseEventHandler<HTMLButtonElement>'.
// Types of parameters 'event' and 'event' are incompatible.
// Type 'MouseEvent<HTMLButtonElement, MouseEvent>' is missing the following
// properties from type 'MouseEvent': offsetX, offsetY, x, y,
// and 14 more.(2322)
}

React uses its own event system, which we refer to as synthetic events. Synthetic events are cross-browser wrappers around the browser’s native event, with the same interface as its native counterpart, but aligned for compatibility. A change to the type from @types/react makes your callbacks compatible again.

import React from "react";

type WithChildren<T = {}> = T & { children?: React.ReactNode };

type ButtonProps = {
  onClick: (event: React.MouseEvent) => void;
} & WithChildren;

function Button({ onClick, children }: ButtonProps) {
  return <button onClick={onClick}>{children}</button>;
}

The browser’s MouseEvent and React.MouseEvent are different enough for TypeScript’s structural type system, meaning that there are some missing properties in the synthetic counterparts. You can see in the error message above that the original MouseEvent has 18 properties more than React.MouseEvent, some of them arguably important, like coordinates and offsets, which come in really hand if you e.g. want to draw on a canvas.

If you want to access properties from the original event, you can use the nativeEvent property.

function handleClick(event: React.MouseEvent) {
  console.log(event.nativeEvent.offsetX, event.nativeEvent.offsetY);
}

const btn = <Button onClick={handleClick}>Hello</Button>;

Events supported are: AnimationEvent, ChangeEvent, ClipboardEvent, CompositionEvent, DragEvent, FocusEvent, FormEvent, KeyboardEvent, MouseEvent, PointerEvent, TouchEvent, TransitionEvent, WheelEvent. As well as SyntheticEvent, for all other events.

So far, we just apply the correct types to make sure we don’t have any compiler errors. Easy enough. But we’re not only using TypeScript to fulfill the ceremony of applying types for the compiler to shut up, but also to prevent ourselves from situations that might be problematic.

Let’s think about a button again. Or a link (the a element). Those elements are supposed to be clicked, that’s their purpose, but in the browser, click events can be received by every element. Nothing keeps you from adding onClick to a div element, the element that has the least semantic meaning of all elements, and no assistive technology will tell you that a div can receive a MouseEvent unless you add lots and lots of additional attributes to it.

Wouldn’t it be great if we can prevent our colleagues and ourselves to use the defined event handlers on the wrong elements? React.MouseEvent is a generic type that takes compatible elements as its first type. This is set to Element, which is the base type for all elements in the browser. But you are able to define a smaller set of compatible elements by sub-typing this generic parameter.

type WithChildren<T = {}> = T & { children?: React.ReactNode };

// Button maps to an HTMLButtonElement
type ButtonProps = {
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
} & WithChildren;

function Button({ onClick, children }: ButtonProps) {
  return <button onClick={onClick}>{children}</button>;
}

// handleClick accepts events from HTMLButtonElement or HTMLAnchorElement
function handleClick(
  event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>
) {
  console.log(event.currentTarget.tagName);
}

let button = <Button onClick={handleClick}>Works</Button>;
let link = <a href="/" onClick={handleClick}>Works</a>;

let broken = <div onClick={handleClick}>Does not work</div>;
//                ^
// Type '(event: MouseEvent<HTMLButtonElement | HTMLAnchorElement,
// MouseEvent>) => void' is not assignable to type
//'MouseEventHandler<HTMLDivElement>'.
// Types of parameters 'event' and 'event' are incompatible.
// Type 'MouseEvent<HTMLDivElement, MouseEvent>' is not assignable to
// type 'MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>'.
// Type 'HTMLDivElement' is not assignable to type #
// 'HTMLButtonElement | HTMLAnchorElement'.

Where React’s types give you more flexibility in some areas, it lacks features in others. For example, the browser native InputEvent is not supported in @types/react. The synthetic event system is meant as a cross-browser solution, and some of React’s compatible browsers still lack implementation of InputEvent. Until they catch up, it’s safe for you to use the base event SyntheticEvent.

function onInput(event: React.SyntheticEvent) {
  event.preventDefault();
  // do something
}

const inp = <input type="text" onInput={onInput} />;

Now you get at least some type safety.

10.8 Typing Polymorphic Components

Problem

You create a proxy component (see Recipe 10.1) that needs to behave as one of many different HTML elements. It’s hard to get the right typings.

Solution

Assert forwarded properties as any or use the JSX factory React.createElement directly.

Discussion

A common pattern in React is to define polymorphic (or as) components, which pre-define behavior, but can act as different elements. Think of a call-to-action button, or CTA, which can either be a link to a website, or an actual HTML button. If you want to style them similarly, they should behave alike, but depending on the context they should have the right HTML element for the right action.

Note

Selecting the right element is an important accessibility factor. a and button elements represent something users can click, but the semantics of a are fundamentally different from the semantics of a button. a is short for anchor, and needs to have a reference (href) to a destination. A button can be clicked, but the action is usually scripted via JavaScript. Both elements can look the same, but act differently. But not only do they act differently, but they also are announced differently using assistive technologies, like screen readers. Think about your users and select the right element for the right purpose.

The idea is that you have an as prop in your component that selects the element type. Depending on the element type of as, you can forward properties that fit the element type. Of course, you can combine this pattern with everything you’ve seen in Recipe 10.1.

<Cta as="a" href="https://typescript-cookbook.com">
  Hey hey
</Cta>

<Cta as="button" type="button" onClick={(e) => { /* do something */ }}>
  My my
</Cta>

When throwing TypeScript into the mix, you want to make sure that you get autocomplete for the right props, and errors for the wrong properties. If you add an href to a button, TypeScript should give you the correct squiggly lines.

// Type '{ children: string; as: "button"; type: "button"; href: string; }'
// is not assignable to type 'IntrinsicAttributes & { as: "button"; } &
// ClassAttributes<HTMLButtonElement> &
// ButtonHTMLAttributes<HTMLButtonElement> & { ...; }'.
// Property 'href' does not exist on type ... (2322)
//                             v
<Cta as="button" type="button" href="" ref={(el) => el?.id}>
  My my
</Cta>

Let’s try to type Cta. First, we develop the component without types at all. In JavaScript, things don’t look too complicated.

function Cta({ as: Component, ...props }) {
  return <Component {...props} />;
}

We extract the as prop and rename it as Component. This is a destructuring mechanism from JavaScript that is syntactically similar to a TypeScript annotation but works on destructured properties, and not on the object itself (where you’d need a type annotation). So don’t be irritated. We rename it to an uppercase component so we can instantiate it via JSX. The remaining props will be collected in ...props, and spread out when creating the component. Note that you can also spread out children with ...props, a nice little side effect of JSX.

When we want to type Cta, we create a CtaProps type that works on either "a" elements or "button" elements and takes the remaining props from JSX.IntrinsicElements. Very similar to what we’ve seen in Recipe 10.1.

type CtaElements = "a" | "button";

type CtaProps<T extends CtaElements> = {
  as: T;
} & JSX.IntrinsicElements[T];

When we wire up our types to Cta, we see that the function signature works very well with just a few extra annotations. But when instantiating the component, we get quite an elaborate error that tells us how much is going wrong!

function Cta<T extends CtaElements>({
  as: Component,
  ...props
}: CtaProps<T>) {
  return <Component {...props} />;
//        ^
// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }'
// is not assignable to type 'IntrinsicAttributes &
// LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement> &
// AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...> &
// ButtonHTMLAttributes<...>>'.
// Type 'Omit<CtaProps<T>, "as" | "children"> & { children: ReactNode; }' is not
//  assignable to type 'LibraryManagedAttributes<T, ClassAttributes<HTMLAnchorElement>
//  & AnchorHTMLAttributes<HTMLAnchorElement> & ClassAttributes<...>
//  & ButtonHTMLAttributes<...>>'.(2322)
}

So where does this message come from? For TypeScript to work correctly with JSX, we need to resort to type definitions in a global namespace called JSX. If this namespace is in scope, TypeScript knows which elements that aren’t components can be instantiated, and which attributes they can accept. These are the JSX.IntrinsicElements we use in this example and in Recipe 10.1.

One type that also needs to be defined is LibraryManagedAttributes. This type is used to provide attributes that are either defined by the framework itself (like key), or via means like defaultProps.

export interface Props {
  name: string;
}

function Greet({ name }: Props) {
  return <div>Hello {name.toUpperCase()}!</div>;
}
// Goes into LibraryManagedAttributes
Greet.defaultProps = { name: "world" };

// Type-checks! No type assertions needed!
let el = <Greet key={1} />;

React’s typings solve LibraryManagedAttributes by using a conditional type. And as we see in Recipe 12.7, conditional types won’t be expanded with all possible variants of a union type when being evaluated. This means that TypeScript won’t be able to check that your typings fit the components because it won’t be able to evaluate LibraryManagedAttributes.

One workaround for this is to assert props to any.

function Cta<T extends CtaElements>({
  as: Component,
  ...props
}: CtaProps<T>) {
  return <Component {...(props as any)} />;
}

Which works, but is a sign of an unsafe operation which, well, shouldn’t be unsafe. Another way is to not use JSX in this case, but rather use the JSX factory React.createElement.

Every JSX call is syntactic sugar to a JSX factory call.

<h1 className="headline">Hello World</h1>

// will be transformed to
React.createElement("h1", { className: "headline" }, ["Hello World"]);

If you use nested components, the third parameter of createElement will contain nested factory function calls. React.createElement is much easier to call than JSX, and TypeScript won’t resort to the global JSX namespace when creating new elements. Sounds like a perfect workaround for our needs.

React.createElement needs three arguments: The component, the props, and the children. Right now, we’ve smuggled all child components with props, but for React.createElement we need to be explicit. This also means that we need to explicitly define children.

For that, we create a WithChildren<T> helper type. It takes an existing type and adds optional children in form of React.ReactNode.

type WithChildren<T = {}> = T & { children?: React.ReactNode };

WithChildren is highly flexible. We can either wrap the type of our props with it.

type CtaProps<T extends CtaElements> = WithChildren<{
  as: T;
} & JSX.IntrinsicElements[T]>;

Or we create a union.

type CtaProps<T extends CtaElements> = {
  as: T;
} & JSX.IntrinsicElements[T] & WithChildren;

Since T is set to {} by default, the type becomes universally usable. This makes it a lot easier for you to attach children whenever you need them. As a next step, we destructure children out of props, and pass all arguments into React.createElement.

function Cta<T extends CtaElements>({
  as: Component,
  children,
  ...props
}: CtaProps<T>) {
  return React.createElement(Component, props, children);
}

And with that, your polymorphic component accepts the right parameters without any errors.