Effective TypeScript for React Applications
Tags: react, javascript, typescript, webdev
Published: 2021-09-14 19:58:38

---
title: Effective TypeScript for React Applications
published: true
date: 2021-09-13 17:33:00 UTC
tags: react,javascript, typescript, webdev
canonical_url: https://onesignal.com/blog/effective-typescript-for-react-applications/
---

 ![Effective TypeScript for React Applications](https://onesignal.com/blog/content/images/2021/09/effective-typescript-for-react-applications.png)

TypeScript and React are excellent in combination. When you sprinkle React’s expressive component model with a little strong typing, you get fewer bugs and an improved developer experience.

That said, the amount of techniques and nuances baked into the TypeScript/React combo can be overwhelming. TypeScript offers so much flexibility in structuring your React components that it’s hard to know what’s best in which situation. Those familiar with JavaScript may find themselves banging their heads against stubborn compiler warnings. Others may dread the boilerplate mountain that looms over every component.​

No matter your level of experience with TypeScript, this guide is here to help you out. We’ve listed below our best practices for working with TypeScript and React, which are based on the same patterns used in our core product.

# Use Interfaces Until You Need a Type

For the most part, you can use type aliases and interfaces interchangeably, as the [difference between the two](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces) is subtle. You can extend existing interfaces with new properties, whereas type aliases are off-limits after declaration. Despite their similarity, it’s still useful to define a pattern so that you’re using the two styles consistently.

Here at OneSignal, we follow the heuristic “use interface until you need to use features from type". We recommend interfaces because they offer a familiar syntax for inheritance that mirrors ES2015 classes. In contrast, types are used to alias primitive values or create unions from other types.

```javascript
type ButtonKind = "primary" | "secondary";
​
interface Props {
  kind: ButtonKind;
}
```

# Unions Over Enums

> Unlike most TypeScript features, [enum] is not a type-level addition to JavaScript but something added to the language and runtime. Because of this, it’s a feature which you should know exists, but maybe hold off on using unless you are sure.  
> —[TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums)

Union types and enums tend to occupy the same space in React in that they both enforce that a particular prop must be one value of a given set. However, we recommend union types over enums for a number of reasons:

- They are compiler-only constructs, so they won't end up in your application's JS bundle.
- They are extensible to other union types.
- They are less verbose.

```javascript
// Avoid enums:
enum ButtonKind {
  PRIMARY = "primary",
  SECONDARY = "secondary",
}

// Prefer union types:
type ButtonKind = "primary" | "secondary";

// Extensible to other union types:
type ExtendedButtonKind = ButtonKind | "tertiary";
```

Now, there are a few caveats to consider with this rule.

For one, `const enum` is a compile-time only enum that doesn’t increase the size of your JS bundle. Unfortunately, `const enum` is a [disabled keyword](https://www.typescriptlang.org/tsconfig#isolatedModules) for certain build tools, like Babel or esbuild.

You should also consider that union types and enums are not syntactically the same. You can reference the value of an enum by its declaration, avoiding direct references to a string literal. If this behavior is desirable, look to regular JS objects instead. With a handy utility type from [type-fest](https://github.com/sindresorhus/type-fest), you can achieve the same behavior.

```javascript
const ButtonStyle = {
  PRIMARY: "primary",
  SECONDARY: "secondary",
} as const;

type ButtonStyleType = ValueOf<typeof ButtonStyle>;

```

# Extending Native HTML Elements

TypeScript ships with tons of helper types that cut down boilerplate for common React idioms. These types are particularly useful when extending native HTML elements like `button` or `input`, where you’ll want to maintain the component's original props to ensure extensibility.

Start by implementing a `Button` component in the two most important use-cases: clicking the button and defining its text. When typing everything manually, you get the following result:

```javascript
import React from "react";

interface Props {
  children: React.ReactNode;
  onClick: () => void;
}

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

```

The first helper type to use here is `React.PropsWithChildren`, which automatically adds the `children` prop to the component:

```javascript
import React from "react";

type Props = React.PropsWithChildren<{
  onClick: () => void;
}>;

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

```

`Button` is looking better, but the component still has to redefine props that are native to the HTML element, like `onClick`. This is a big problem for foundational components like `Button` that make up your application’s design system, as their props will grow wildly with their usage.

```javascript
// This list tends to grow quickly!
type Props = React.PropsWithChildren<{
  onClick: () => void;
  type: "submit" | "button" | "reset" | undefined;
  disabled: boolean;
  // ...
}>;

```

Luckily, TypeScript has another utility designed for this exact purpose.

`ComponentPropsWithoutRef` is a generic type that supplies props for built-in React handlers and native HTML attributes. By passing in `"button"` as the template, you specify that the component is extending the HTML `button` element.

```javascript
import React from "react";

type Props = React.ComponentPropsWithoutRef<"button">;

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

```

The result is a component that is clean and extensible. If additional props are needed, swap the `type` for an `interface`:

```javascript
import React from "react";

interface Props extends React.ComponentPropsWithoutRef<"button"> {
  specialProp: number;
}

function Button({ children, onClick, type, specialProp }: Props) {
  // ...
}

```

# Type Refinement and Disjoint Unions

Disjoint unions (or [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)) are a powerful feature that can help you refine the component props of complex structures. In short, they allow your component to support multiple variants of a shared interface.

Consider a `Button` component that has several theme variations, such as "primary" and "secondary". You can express this type as a `string` and pass it down as props.

```javascript
interface Props {
  kind: string;
}

function getStyles(kind: string) {
  switch (kind) {
    case "primary":
      return styles.primary;
    case "secondary":
      return styles.secondary;
    default:
      throw new Error("invalid button kind");
  }
}

function Button({ kind }: Props) {
  const styles = getStyles(kind);
  // ...
}

```

While this implementation is simple, it presents significant problems.

For one, you can pass any string value into the component props, even though only "primary" and "secondary" are implemented by the component. TypeScript won't warn you that any other value throws an error.

```javascript
// This passes compiler checks, yet throws an application error!
function App() {
  return <Button kind="not-a-style">click me!</Button>;
}

```

You can instead switch `string` to a union type, which offers much-needed improvements. The union type informs TypeScript of all possible values for `kind`, preventing any unhandled cases.

```javascript
type ButtonKind = "primary" | "secondary";

interface Props {
  kind: ButtonKind;
}

function getStyles(kind: ButtonKind) {
  switch (kind) {
    case "primary":
      return styles.primary;
    case "secondary":
      return styles.secondary;
    // Default case is no longer needed!
  }
}

function Button({ kind }: Props) {
  const styles = getStyles(kind);
  // ...
}

```

Looking back at the component instance that is passed an invalid string literal, TypeScript now [offers a helpful error](https://www.typescriptlang.org/play#code/C4TwDgpgBAQgrsYB7AdgaQJYoCZQLxQDkYAThgLYCGJIhUAPkQM4QDGq21tAUN1sBBIAzSq2gAFEkjBMoEAB4CcsgEoRRwAHQBhJOTCoIKYJOlMA6hmAALJAjVCAPIQBGCZCkIA+KAG9uUFAA1ljYAFyw7qiYONwAvrxCcCiswBioUADmEMAAyqAANhBMABQhOBHwiNGhAJR+AVBMAO5WrNZQZXUNgYGslCxEpBRchGGNvVAkOXAkKENkVDQAtEyFEIQTUP2DhCzsOKPjk4HTwLPze2wcXKvrm4EJCdxJKWkZVR4lvsGhADTbawYArYabzOIRUwyer+PqoNZNdayAjZPJIro4Wq8U4zOZQRxuarzVgFAZMABylHIEDwvjWICKTDiXl87WBoKMcUcAHpCR4vPFEslUul5gBBMBgEowxpnC74z4ZcrYPAAIhQSGAy0odwZEFVXhJGFYQSg1IAhDzFSgBQkgA):

```javascript
// Error: Type '"not-a-style"' is not assignable to type 'ButtonKind'
function App() {
  return <Button kind="not-a-style">click me!</Button>;
}

```

Union types are great at refining props for primitive values. But what about more complex structures?

Consider that the "primary" button requires a special method, `specialPrimaryMethod`, that is not supported by the “secondary” variant. The component calls this special method when handling a click.

```javascript
type ButtonKind = "primary" | "secondary";

interface Props extends React.ComponentPropsWithoutRef<"button"> {
  kind: ButtonKind;
  specialPrimaryMethod?: () => void;
}

function Button({ kind, onClick, specialPrimaryMethod, children }: Props) {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (kind === "primary") {
      if (specialPrimaryMethod) {
        specialPrimaryMethod();
      }
    } else {
      onClick?.(e);
    }
  };

  return <button onClick={handleClick}>{children}</button>;
}
```

Although this component compiles, the type definition of props doesn’t inform the TypeScript compiler when `specialPrimaryMethod` is permitted. The TypeScript compiler thinks that both “primary” and “secondary” allow the method, and that the method is optional in either case.

To further demonstrate why this is problematic, take a look at the following instances of the component. The TypeScript compiler considers all of them valid, even though some of them conflict with the intended implementation.

```html
// Correct use-case
<Button kind="primary" specialPrimaryMethod={doSpecial}>...

// Invalid use-case: specialPrimaryMethod shouldn't be optional
<Button kind="primary">...

// Invalid use-case: secondary shouldn't support specialPrimaryMethod
<Button kind="secondary" specialPrimaryMethod={doSpecial}>...

```

This is where the disjoint union comes in handy. By splitting apart the interfaces for the “primary” variant and the “secondary” variant, you can achieve better compile-time type-checking.

```javascript
type ButtonKind = "primary" | "secondary";

// Build separate interfaces for Primary & Secondary buttons
interface PrimaryButton {
  kind: "primary";
  specialPrimaryMethod: () => void;
}

interface SecondaryButton {
  kind: "secondary";
}

// Create a disjoint union
type Button = PrimaryButton | SecondaryButton;

// Add built-in HTML props to the disjoin union
type Props = React.ComponentPropsWithoutRef<"button"> & Button;

// You can no longer destructure props since specialPrimaryMethod
// doesn't always exist on the object.
function Button(props: Props) {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (props.kind === "primary") {
      // No extra if check needed!
      props.specialPrimaryMethod();
    } else {
      props.onClick?.(e);
    }
  };

  return <button onClick={handleClick}>{props.children}</button>;
}

```

The additional type refinement provided by the disjoint union now [appropriately throws compile-time errors](https://www.typescriptlang.org/play#code/C4TwDgpgBAQgrsYB7AdgaQJYoCZQLxQDkYAThgLYCGJIhUAPkQM4QDGq21tAUN1sBBIAzSq2gAFMlRrxEqKAG9uUKAGss2AFxFSFLoWVQmkVhkoAbSXpoBZCMAAWSLVAAUASnwA+KADckGNjcAL68-IIiYlAAymwcXLLIKIqG6jjahCzsOPohvKCQsAhJ+FBW0iCJ8oyx2Zwyxaj54BIkSGBMpQBKEKLAAHQAwkjkYKgQKMCS7UwA6hiOSAg9QgA8hABGjSiEPgBkRXIovEJwKKzAGPJVKK6kM9rTHZ5KKtlMwFAOlDjmEIPmDCsVSlVwQbQ9Pr9GxLFgAUV8E2AqwAEgAVGwAGRucL+5CRXk8eB8rxUUAwQjc9w6-TSuDwDJ0Un0L0MZKg1KY-WMbDMlmZtnsTmwHjZUGCUAg5hYKXZKk5-VQAKBqgA-P0we4xaEVKFDCR7HASMlVlsjlAlYDgXgFN9fv8rapgl4FArWA4MOZsAaUMFVgB6M1JLx5binc6XeQAQTAYA8sqgBuARuSrjFqxDcqgqxuag0eAARLoKgWvKxHVB8QBCAM3TNynPbPM4QvFrgFowmPnlLh2RbYG3x4mKYLOoRIJC17b19mN810wtZeI0Ds80wWHuC-uDokk0deceT-119P++ta0JAA) for invalid cases.

```html
// All good!
<Button kind="primary" specialPrimaryMethod={() => {}}>foo</Button>

// Error: Property 'specialPrimaryMethod' is missing
<Button kind="primary">click me!</Button>

// Error: Type '{ ... specialPrimaryMethod: () => void; }' is not assignable
<Button kind="secondary" specialPrimaryMethod={() => {}}>click me!</Button>

```

## Don’t Overuse Disjoint Unions

Disjoint unions are a powerful technique that enable general-purpose components. However, take care not to overuse them as they can lead to highly complex components.

Anytime you reach for disjoint unions, pause and consider whether the single component should instead be separated into two.

# Accessible Components With Polymorphism

Polymorphic components are great for tuning markup for accessibility.

Consider a `Container` component that applies some styles to a `div`. You may want to use this `Container` component in situations that are better described by HTML5 elements like [aside](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside) or [section](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section).

Rather than duplicating the `Container` component for slight alterations in its JSX, create a polymorphic component. This is as simple as including a new prop, `as`, that accepts a union of HTML element strings.

```javascript
import React from "react";

type Props = React.PropsWithChildren<{
  as: "div" | "section" | "aside";
}>;

function Container({ as: Component = "div", children }: Props) {
  return <Component className={styles.container}>{children}</Component>;
}

```

The destructured alias, `{ as: Component }`, is a convention that helps illustrate that the prop is a React component and not just a string.

The `Container` component now supports different HTML5 elements to better fit its use-case.

```html
<Container as="section">
  <p>section content</p>
</Container>

```

# Watch Out for These Bad Practices

## Using `defaultProps`

Defining `defaultProps` on function components [is marked for deprecation](https://twitter.com/dan_abramov/status/1133878326358171650). You should instead assign defaults with prop destructuring:

**Prefer**

```javascript
interface Props {
  color?: string;
}

function Button({ color = "red" }: Props) {
  /* ... */
}

```

**Avoid**

```javascript
type Props = { color: string } & typeof defaultProps;

const defaultProps = { color: "red" };

function Button(props: Props) {
  /* ... */
}

Button.defaultProps = defaultProps;

```

## Using the Non-Null Assertion Operator

The [non-null assertion operator](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-) subverts TypeScript’s ability to check `null` values. Despite how easy it is to type, this operator can cause a ton of harm. Let TypeScript do its job!

In this case, you should instead rely on [refinement](https://paper.dropbox.com/doc/TypeScript-React-Best-Practices--BRJMvtoy~KZbGLSMOvUEiMDyAg-DSpTU8995rN7zqSROwCsl) or [narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) and avoid overriding the TypeScript compiler.

**Prefer**

```javascript
const liveSafely = (x?: number) => {
  if (typeof x !== 'number') {
    throw new Error('number expected')
  }
  return x.toFixed(2)
}}

```

**Avoid**

```javascript
const liveDangerously = (x?: number) => {
  return x!.toFixed(2);
};

```

## Raising Exceptions for Exhaustive Cases

There are few cases that call for raising an exception within a React component, since it renders a blank screen if handled improperly. You can avoid catastrophic failures with [Error Boundaries](https://reactjs.org/docs/error-boundaries.html), but in most cases throwing an exception is unnecessary.

Instead, default to the closest acceptable user interface.

**Prefer**

```javascript
switch (props.kind) {
  case "primary":
  default:
    return { color: "red" };
}

```

**Avoid**

```javascript
switch (props.kind) {
  // ...
  default:
    throw new Error("unsupported option");
}

```

At OneSignal, we love empowering developers to refine their workflow and build great software. We hope these patterns help evolve your React and TypeScript codebase.

## >>We’re hiring! Check out our open positions and apply on our careers page: [https://onesignal.com/careers](https://onesignal.com/careers)>>