No history yet

Advanced TypeScript Features

Combining Types

As your React applications grow, you'll often find the need for types that are more flexible than simple primitives. TypeScript allows you to combine existing types to create new, more powerful ones. The two primary ways to do this are with union and intersection types.

A union type describes a value that can be one of several types. It's like saying a variable can be a string or a number.

We use the pipe symbol (|) to create a union. This is incredibly useful for component props that need to accept different kinds of values while still being type-safe.

// This component's ID can be either a number or a string.
type ItemProps = {
  id: number | string;
  name: string;
};

function Item(props: ItemProps) {
  return <li>Item {props.id}: {props.name}</li>;
}

On the other hand, an intersection type combines multiple types into one. A value of an intersection type will have all the properties of all the types in the intersection. You create one using the ampersand symbol (&).

Think of intersections as mixing two ingredients to get a combination of both. If you have a User type and a Permissions type, you can create an AdminUser that has properties from both.

type User = {
  id: number;
  username: string;
};

type Permissions = {
  canEdit: boolean;
  canDelete: boolean;
};

// AdminUser has all properties of User AND Permissions
type AdminUser = User & Permissions;

const admin: AdminUser = {
  id: 1,
  username: 'jane_doe',
  canEdit: true,
  canDelete: false,
};

Checking Types at Runtime

Union types are great for flexibility, but they introduce a challenge. If a function receives a value that could be a string or a number, how do you safely perform operations on it? You can't just call a string method like .toUpperCase() on it, because it might be a number. This is where type guards come in.

Type Guard

noun

An expression that performs a runtime check that guarantees the type in a certain scope.

TypeScript understands common JavaScript operators like typeof and in as type guards. Inside a conditional block where you've checked the type, TypeScript will narrow the type for you, allowing you to access type-specific properties safely.

function formatId(id: string | number) {
  if (typeof id === 'string') {
    // TypeScript knows `id` is a string here.
    return id.toUpperCase();
  }
  // And here, it knows `id` must be a number.
  return `USER-${id}`;
}

For objects, the in operator is a useful type guard. It checks for the existence of a property on an object.

type Student = { name: string; studentId: number };
type Teacher = { name: string; employeeId: number };

function printPersonInfo(person: Student | Teacher) {
  console.log(`Name: ${person.name}`);

  // Use the 'in' operator as a type guard
  if ('studentId' in person) {
    console.log(`Student ID: ${person.studentId}`);
  } else {
    console.log(`Employee ID: ${person.employeeId}`);
  }
}

Building Flexible Components

One of the goals of building components in React is reusability. But what if you want to create a component that works with different data types while maintaining type safety? For example, a dropdown menu component that could display a list of users, products, or anything else. This is the perfect job for generics.

Generics are like placeholders for types. They allow you to write a function or component that can work over a variety of types rather than a single one.

You define a generic with angle brackets (<>). Inside, you put a variable, conventionally T, to represent the type that will be provided later. Let's build a generic List component that can render an array of any type of object, as long as each object has an id property.

import React from 'react';

// We constrain T to be an object with an id that is a string or number
interface ListItem {
  id: string | number;
}

// We use <T extends ListItem> to define our generic type
interface ListProps<T extends ListItem> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

// The component itself is a generic function
function List<T extends ListItem>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

With this generic List component, we can now use it to render a list of users, products, or any other data structure, and TypeScript will ensure we're accessing properties correctly inside our renderItem function.

Handy Type Shortcuts

TypeScript comes with a set of built-in utility types that help you manipulate your existing types without having to write them from scratch. They are essentially generic types that perform common transformations.

Utility TypeDescription
Partial<T>Makes all properties of type T optional.
Required<T>Makes all properties of type T required.
Readonly<T>Makes all properties of type T read-only.
Pick<T, K>Creates a new type by picking a set of properties K from T.
Omit<T, K>Creates a new type by removing a set of properties K from T.

Let's see how Partial can be useful. Imagine a form for updating a user's profile. The user might only change their email and not their name. The update function would only need a subset of the full User properties. Partial<User> is perfect for this.

interface User {
  id: number;
  name: string;
  email: string;
}

// The function accepts an object where all properties of User are optional.
function updateUser(id: number, updates: Partial<User>) {
  // ... logic to update user in the database
}

// This is a valid call
updateUser(1, { email: 'new.email@example.com' });

Similarly, Pick and Omit are fantastic for creating props for smaller, reusable components from larger data models. For example, you can Pick just the name and avatarUrl from a large User object to create the props for a simple UserAvatar component.

interface User {
  id: number;
  name: string;
  email: string;
  avatarUrl: string;
  lastLogin: Date;
}

// UserAvatarProps will only have `name` and `avatarUrl`
type UserAvatarProps = Pick<User, 'name' | 'avatarUrl'>;

function UserAvatar(props: UserAvatarProps) {
  return <img src={props.avatarUrl} alt={props.name} />;
}

Mastering these advanced features will help you write cleaner, more maintainable, and highly reusable components in your React and TypeScript projects.

Time to check what you've learned.