Basarat Ali Syed That guy you probably know because you heard something about TypeScript.

Using built-in utility types in TypeScript

8 min read 2517

Typescript Logo Over a Wooden Fence Background

TypeScript has one of the most powerful type systems of any programming language out there — mostly because it evolved to accommodate all the dynamic things you can do in JavaScript.

Including features such as conditional, lookup, and mapped types mean that you can write some pretty advanced type functions in TypeScript.

What is a type function?

TypeScript allows you to create a type alias from any existing type. For example:

// Example
type Str = string; 

// Usage
let message: Str = 'Hello world';

TypeScript type aliases also support generics. Generics are traditionally used to constrain one type based on another. For example, the type for a value, setValue pair, and a generic is used to ensure that setValue is always invoked with the same type used by value:

// Example
type ValueControl<T> = {
  value: T,
  setValue: (newValue: T) => void,
};

// Usage 
const example: ValueControl<number> = {
  value: 0,
  setValue: (newValue) => example.value = newValue,
};

Notice that in the above example we can pass in a type for ValueControl. In our example, we are passing in the type number (i.e., ValueControl<number>).

The truly powerful feature of TypeScript is that the type passed into the generic function can be used in conditions (with conditional types) and mapping (with mapped types). Here is an example of a type that uses conditions to exclude nulland undefined from a type:

/**
 * Exclude null and undefined from T
 */
type NoEmpty<T> = T extends null | undefined ? never : T;

// Usage 
type StrOrNull = string | null;
type Str = NoEmpty<StrOrNull>; // string

However, you don’t necessarily need to use these base-level features, as TypeScript also comes with a number of handy built-in utility functions.

In fact, our type NoEmpty<T> already ships as a part of TypeScript (it’s called NonNullable<T> and we cover it below). In this article, we’ll cover these types of functions with real-world examples to see why you would want to use them.

Built-in type functions in TypeScript

As of TypeScript 4.0, these are the built-in type functions that you can use in TypeScript without needing any additional packages:

Partial<T>

Partial marks all the members of an input type T as being optional. Here is an example with a simple Point type:

We made a custom demo for .
No really. Click here to check it out.

type Point = { x: number, y: number };

// Same as `{ x?: number, y?: number }`
type PartialPoint = Partial<Point>;

A common use case is the update pattern found in many state management libraries, where you only provide a subset of the properties you want changed. For example:

class State<T> {
  constructor(public current: T) { }
  // Only need to pass in the properties you want changed
  update(next: Partial<T>) {
    this.current = { ...this.current, ...next };
  }
}

// Usage
const state = new State({ x: 0, y: 0 });
state.update({ y: 123 }); // Partial. No need to provide `x`.
console.log(state.current); // Update successful: { x: 0, y: 123 }

Required<T>

Required does the opposite of Partial<T>. It makes all the members of an input type T non-optional. In other words, it makes them required. Here is an example of this transformation:

type PartialPoint = { x?: number, y?: number };

// Same as `{ x: number, y: number }`
type Point = Required<PartialPoint>;

A use case is when a type has optional members but portions of your code need all of them to be provided. You can have a config with optional members, but internally, you initialize them so you don’t have to handle null checking all your code:

// Optional members for consumers
type CircleConfig = {
  color?: string,
  radius?: number,
}

class Circle {
  // Required: Internally all members will always be present
  private config: Required<CircleConfig>;

  constructor(config: CircleConfig) {
    this.config = {
      color: config.color ?? 'green',
      radius: config.radius ?? 0,
    }
  }

  draw() {
    // No null checking needed :)
    console.log(
      'Drawing a circle.',
      'Color:', this.config.color,
      'Radius:', this.config.radius
    );
  }
}

Readonly<T>

This marks all the properties of input type T as readonly. Here is an example of this transformation:

type Point = { x: number, y: number };

// Same as `{ readonly x: number, readonly y: number }`
type ReadonlyPoint = Readonly<Point>;

This is useful for the common pattern of freezing an object to prevent edits. For example:

function makeReadonly<T>(object: T): Readonly<T> {
  return Object.freeze({ ...object });
}

const editablePoint = { x: 0, y: 0 };
editablePoint.x = 2; // Success: allowed

const readonlyPoint = makeReadonly(editablePoint);
readonlyPoint.x = 3; // Error: readonly

Pick<T, Keys>

Picks only the specified Keys from T. In the following code, we have a Point3D with keys 'x' | 'y' | 'z', and we can create a Point2D by only picking the keys 'x' | 'y' :

type Point3D = {
  x: number,
  y: number,
  z: number,
};

// Same as `{ x: number, y: number }`
type Point2D = Pick<Point3D, 'x' | 'y'>;

This is useful for getting a subset of objects like we’ve seen in the above example by creating Point2D.

A more common use case is to simply get the properties you are interested in. This is demonstrated below, where we get the width and height from all the CSSProperties:

// All the CSSProperties
type CSSProperties = {
  color?: string,
  backgroundColor?: string,
  width?: number,
  height?: number,
  // ... lots more
};

function setSize(
  element: HTMLElement,
  // Usage: Just need the size properties
  size: Pick<CSSProperties, 'width' | 'height'>
) {
  element.setAttribute('width', (size.width ?? 0) + 'px');
  element.setAttribute('height', (size.height ?? 0) + 'px');
}

Record<Keys, Value>

Given a set of member names specified by Keys, this creates a type where each member is of type Value. Here is an example that demonstrates this:

// Same as `{x: number, y: number}`
type Point = Record<'x' | 'y', number>;

When all the members of a type have the same Value, using Record can help your code read better because it’s immediately obvious that all members have the same Value type. This is slightly visible in the above Point example.

When there is a large number of members, then Record is even more useful. Here’s the code without using Record:

type PageInfo = {
  id: string,
  title: string,
};

type Pages = {
  home: PageInfo,
  services: PageInfo,
  about: PageInfo,
  contact: PageInfo,
};

Here’s the code using Record:

type Pages = Record<
  'home' | 'services' | 'about' | 'contact',
  { id: string, title: string }
>;

Exclude<T, Excluded>

This excludes Excluded types from T.

type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
type T2 = Exclude<string | number | (() => void), Function>; // string | number

The most common use case is to exclude certain keys from an object. For example:

type Dimensions3D = 'x' | 'y' | 'z';
type Point3D = Record<Dimensions3D, number>;

// Use exclude to create Point2D
type Dimensions2D = Exclude<Dimensions3D, 'z'>;
type Point2D = Record<Dimensions2D, number>;

You can also use it to exclude other undesirable members (e.g., null and undefined) from a union:

type StrOrNullOrUndefined = string | null | undefined;

// string
type Str = Exclude<StrOrNullOrUndefined, null | undefined>;

NonNullable<T>

This excludes null and undefined from the type T. It has the same effect as Exclude<T, null | undefined>.

A quick JavaScript premier: nullable is something that can be assigned a nullish value i.e., null or undefined. So, non-nullable is something that shouldn’t accept nullish values.

Here is the same example we saw with Exclude, but this time, let’s use NonNullable:

type StrOrNullOrUndefined = string | null | undefined;

// Same as `string`
// Same as `Exclude<StrOrNullOrUndefined, null | undefined>`
type Str = NonNullable<StrOrNullOrUndefined>;

Extract<T, Extracted>

This extracts Extracted types from T. You can view it as an opposite of Exclude because, instead of specifying which types you want to exclude (Exclude), you specify which types you want to keep/extract (Extract):

type T0 = Extract<'a' | 'b' | 'c', 'a'>; // 'a'
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T2 = Extract<string | number | (() => void), Function>; // () => void

Extract can be thought of as an intersection of two given types. This is demonstrated below where the common elements 'a' | 'b' are extracted:

type T3 = Extract<'a' | 'b' | 'c', 'a' | 'b' | 'd'>; // 'a' | 'b'

One use case of Extract is to find the common base of two types, like so:

type Person = {
  id: string,
  name: string,
  email: string,
};

type Animal = {
  id: string,
  name: string,
  species: string,
};

/** Use Extract to get the common keys */
type CommonKeys = Extract<keyof Person, keyof Animal>;

/** 
 * Map over the keys to find the common structure 
 * Same as `{ id: string, name: string }`
 **/
type Base = {
  [K in CommonKeys]: (Animal & Person)[K]
};

Omit<T, Keys>

Omits the keys specified by Keys from the type T. Here’s an example:

type Point3D = {
  x: number,
  y: number,
  z: number,
};

// Same as `{ x: number, y: number }`
type Point2D = Omit<Point3D, 'z'>;

Omitting certain properties from an object before passing it on is a common pattern in JavaScript.

The Omit type function offers a convenient way to annotate such transforms. It is, for example, conventional to remove PII (personally identifiable information such as email addresses and names) before logging in. You can annotate this transform with Omit.

type Person = {
  id: string,
  hasConsent: boolean,
  name: string,
  email: string,
};

// Utility to remove PII from `Person`
function cleanPerson(person: Person): Omit<Person, 'name' | 'email'> {
  const { name, email, ...clean } = person;
  return clean;
}

Parameters<Function>

Given a Function type, this type returns the types of the function parameters as a tuple. Here is an example that demonstrates this transform:

function add(a: number, b: number) {
  return a + b;
}

// Same as `[a: number, b: number]`
type AddParameters = Parameters<typeof add>;

You can combine Parameters with TypeScript’s index lookup types to get any individual parameter. We can even fetch the type of the first parameter:

function add(a: number, b: number) {
  return a + b;
}

// Same as `number`
type A = Parameters<typeof add>[0];

A key use case for Parameters is the ability to capture the type of a function parameter so you can use it in your code to ensure type safety.

// A save function in an external library
function save(person: { id: string, name: string, email: string }) {
  console.log('Saving', person);
}

// Ensure that ryan matches what is expected by `save`
const ryan: Parameters<typeof save>[0] = {
  id: '1337',
  name: 'Ryan',
  email: 'ryan@example.com',
};

ConstructorParameters<ClassConstructor>

This is similar to the Parameters type we saw above. The only difference is that ConstructorParameters works on a class constructor, like this:

class Point {
  private x: number;
  private y: number;
  constructor(initial: { x: number, y: number }) {
    this.x = initial.x;
    this.y = initial.y;
  }
}

// Same as `[initial: { x: number, y: number} ]`
type PointParameters = ConstructorParameters<typeof Point>;

And, of course, the main use case for ConstructorParamters is also similar. In the following example, we use it to ensure that our initial values are something that will be accepted by the Point class:

class Point {
  private x: number;
  private y: number;
  constructor(initial: { x: number, y: number }) {
    this.x = initial.x;
    this.y = initial.y;
  }
}

// Ensure that `center` matches what is expected by `Point` constructor
const center: ConstructorParameters<typeof Point>[0] = {
  x: 0,
  y: 0,
};

ReturnType<Function>

Given a Function type, this gets the type returned by the function.

function createUser(name: string) {
  return {
    id: Math.random(),
    name: name
  };
}

// Same as `{ id: number, name: string }`
type User = ReturnType<typeof createUser>;

A possible use case is similar to the one we saw with Parameters. It allows you to get the return type of a function so you can use it to type other variables. This is actually demonstrated in the above example.

You can also use ReturnType to ensure that the output of one function is the same as the input of another function. This is common in React where you have a custom hook that manages the state needed by a React component.

import React from 'react';

// Custom hook
function useUser() {
  const [name, setName] = React.useState('');
  const [email, setEmail] = React.useState('');
  return {
    name,
    setName,
    email,
    setEmail,
  };
}

// Custom component uses the return value of the hook
function User(props: ReturnType<typeof useUser>) {
  return (
    <>
      <div>Name: {props.name}</div>
      <div>Email: {props.email}</div>
    </>
  );
}

InstanceType<ClassConstructor>

InstanceType is similar to ReturnType we saw above. The only difference is that InstanceType works on a class constructor.

class Point {
  x: number;
  y: number;
  constructor(initial: { x: number, y: number }) {
    this.x = initial.x;
    this.y = initial.y;
  }
}

// Same as `{x: number, y: number}`
type PointInstance = InstanceType<typeof Point>;

You wouldn’t normally need to use this for any static class like the Point class above. This is because you can just use the type annotation Point, as shown here:

class Point {
  x: number;
  y: number;
  constructor(initial: { x: number, y: number }) {
    this.x = initial.x;
    this.y = initial.y;
  }
}

// You wouldn't do this
const verbose: InstanceType<typeof Point> = new Point({ x: 0, y: 0 });

// Because you can do this
const simple: Point = new Point({ x: 0, y: 0 });

However, TypeScript also allows you to create dynamic classes e.g., the following function DisposibleMixin returns a class on the fly:

type Class = new (...args: any[]) => any;

// creates a class dynamically and returns it
function DisposableMixin<Base extends Class>(base: Base) {
  return class extends base {
    isDisposed: boolean = false;
    dispose() {
      this.isDisposed = true;
    }
  };
}

Now we can use InstanceType to get the type of instances created by invoking DisposiblePoint:

type Class = new (...args: any[]) => any;

function DisposableMixin<Base extends Class>(base: Base) {
  return class extends base {
    isDisposed: boolean = false;
    dispose() {
      this.isDisposed = true;
    }
  };
}

// dynamically created class
const DisposiblePoint = DisposableMixin(class {
  x = 0;
  y = 0;
});

// Same as `{isDisposed, dispose, x, y}`
let example: InstanceType<typeof DisposiblePoint>;

Conclusion

As we have seen, there are many built-in utility types that come with TypeScript. Many of these are simple definitions that you can write yourself, e.g., if you wanted to exclude null and undefined you could easily write the following yourself:

// Your custom creation
type NoEmpty<T> = T extends null | undefined ? never : T;

// Usage 
type StrOrNull = string | null;

// string - People need to look at `NoEmpty` to understand what it means
type Str = NoEmpty<StrOrNull>; 

However, using the built-in version NonNullable (which does the same thing) can improve readability in your code so people familiar with TypeScript’s standard library don’t need to parse the body T extends null | undefined ? never : T; in order to understand what is going on.

This is demonstrated below:

// No need for creating something custom

// Usage 
type StrOrNull = string | null;

// string - People that know TS know what `NonNullable` does
type Str = NonNullable<StrOrNull>; 

Ultimately, you should use the built-in types like ReadOnly / Partial / Required whenever possible instead of creating custom ones. In addition to saving time writing code, it also saves you from having to think about naming your utilities, as these have been named for you by the TypeScript team.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Basarat Ali Syed That guy you probably know because you heard something about TypeScript.

2 Replies to “Using built-in utility types in TypeScript”

  1. This is by far the best explanation of Typescripts utility types I have ever read. I also like the sample code which makes it easy to use in your own projects. Thank you very much!

Leave a Reply