Utility Types page

Use utility types provided by TypeScript.

Overview

In this section, you will:

  • Make properties optional with Partial<Type>.
  • Make properties required with Required<Type>.
  • Set properties as immutable with Readonly<Type>.
  • Remove nullability with NonNullable<Type>.
  • Map keys to a type with Record<Keys, Type>.
  • Select included subtypes with Extract<Type, Union>.
  • Exclude values from a type with Exclude<Type, ExcludedUnion>.
  • Include specific properties with Pick<Type, Keys>.
  • Exclude specific properties with Omit<Type, Keys>.
  • Get function return type with ReturnType<Type>.

Objective 1: Property existence modifiers

Using Partial<Type>

Converts all properties of a type to be optional. This is useful when you want to create a type that is a subset of another, with no property being strictly required.

Partial is often used when you need to partially update an object. See the example below:

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

type PartialDinosaur = Partial<Dinosaur>;

// PartialDinosaur is equivalent to:
interface YetPartialDinosaur {
  species?: string;
  diet?: Diet;
  age?: number;
};

// example
function updateDinosaur(
  dinosaur: Dinosaur,
  fieldsToUpdate: Partial<Dinosaur>
): Dinosaur {
  return { ...dino, ...fieldsToUpdate };
};

const oldDino: Dinosaur = {
  species: "Tyrannosaurus rex",
  diet: Diet.Carnivore,
};

const newDino: Dinosaur = updateDinosaur(oldDino, {
  diet: Diet.Omnivore,
});

In the code above, the second parameter for the function updateDinosaur is a partial Dinosaur. This allows us to pass in a Dinosaur object with one or more of the key-value pairs, without having to pass in the entire Dinosaur object.

Using Required<Type>

Converts all optional properties of a type to required, which is the opposite of Partial. You might use it when you can initialize all the properties of an object and want to avoid checking for null or undefined for the optional properties.

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

type RequiredDinosaur = Required<Dinosaur>;

// RequiredDinosaur is equivalent to:
interface YetRequiredDinosaur extends Dinosaur {
  age: number; // turning age property to required
};

const trex: RequiredDinosaur = {
  species: "Tyrannosaurus rex",
  diet: Diet.Carnivore,
  age: 30,
};

if (trex.age > 30) {
  // do something
};

In the code above, we are declaring trex to type RequiredDinosaur. This will help us skip the check if age is null step because it is a required property

Using Readonly<Type>

Makes all properties in a type readonly, meaning that once an object is created, its properties cannot be modified. Use it to prevent objects from being mutated.

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

type NamableDinosaur = { name: string } & Dinosaur; // this is an intersection between { name: string } and Dinosaur. Think { name: string } + Dinosaur
type ReadOnlyDinosaur = Readonly<NamableDinosaur>;

// Meet Bruno, read-only dinosaur
const dino: ReadOnlyDinosaur = {
  name: "Bruno",
  age: 27,
  species: "Tyrannosaurus rex",
  diet: Diet.Carnivore,
};

// Today is its birthday! Let’s attempt to increase its age:
dino.age += 1;
// Oops! TypeScript error

In the code above, we are declaring dino to type ReadOnlyDinosaur. This will prevent us from assigning a new value because it is a read-only object.

Using NonNullable<Type>

Excludes null and undefined from the union of a type, ensuring that a type only contains “non-nullable” values. Useful to prevent any run-time errors from occurring because we forgot to assign to a property.

type Species = "Tyrannosaurus rex" | "Triceratops horridus" | null | undefined;

type NNSpecies = NonNullable<Species>;
// Could also be written as type NNSpecies = 'Tyrannosaurus rex' | 'Triceratops horridus'

In the code above, NNSpecies will not allow null or undefined.

Setup 1

✏️ Create src/utilities/property-existence.ts and update it to be:

type Person = {
  role: "developer";
  email?: string;
  id: number;
  firstName: string;
  lastName: string;
  team: "React" | "Angular" | "backend";
};

export type UpdateablePerson = Person; 

export type FullyDefinedPerson = Person;

export type NonEditablePerson = Person; 

Verify 1

✏️ Create src/utilities/property-existence.test.ts and update it to be:

import {
  NonEditablePerson,
  UpdateablePerson,
  FullyDefinedPerson,
} from "./property-existence";
import { describe, it } from "node:test";

describe("Property existence modifiers", () => {
  it("partial typing works", () => {
    const personToUpdate1: UpdateablePerson = {
      team: "React",
    };
  });
  it("required typing works", () => {
    // @ts-expect-error
    const fullyDefinedPerson: FullyDefinedPerson = {
      role: "developer",
      id: 5,
      firstName: "string",
      lastName: "string",
      team: "React",
    };
  });
  it("readonly typing works", () => {
    const nonEditablePerson: NonEditablePerson = {
      role: "developer",
      email: "string",
      id: 5,
      firstName: "string",
      lastName: "string",
      team: "React",
    };

    // @ts-expect-error
    nonEditablePerson.firstName = "somethingelse";
  });
});

Exercise 1

Update the property-existence.ts file so that:

  • UpdateablePerson allows all properties to be optional
  • FullyDefinedPerson ensures that all properties are defined
  • NonEditablePerson won’t allow any update to a property

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 1

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/utilities/property-existence.ts to be:

type Person = {
  role: "developer";
  email?: string;
  id: number;
  firstName: string;
  lastName: string;
  team: "React" | "Angular" | "backend";
};

export type UpdateablePerson = Partial<Person>;

export type FullyDefinedPerson = Required<Person>;

export type NonEditablePerson = Readonly<Person>;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 2: Construct an object type

Using Record<Keys, Type>

Shortcut for defining an object where the keys are all one type and the values are all one type.

This is particularly useful when you have a type in which multiple keys share the same value Type, so you can avoid repeating the pattern key: type;

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

const dinosCollection: Record<string, Dinosaur> = {
  // Could also be written as Record<'trex' | 'triceratops', Dinosaur>
  trex: {
    species: "Tyrannosaurus rex",
    diet: Diet.Carnivore,
  },
  triceratops: {
    species: "Triceratops horridus",
    diet: Diet.Herbivore,
  },
};

In the code above, dinosCollection is equivalent to:

{
  [key: string]: Dinosaur
}

Setup 2

✏️ Create src/utilities/record.ts and update it to be:

export type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

export type PersonMap = unknown;

Verify 2

✏️ Create src/utilities/record.test.ts and update it to be:

import type { Person, PersonMap } from "./record";
import { describe, it } from "node:test";

describe("record tests", () => {
  it("typing works", () => {
    const people: Person[] = [
      {
        role: "developer",
        email: "email@developer.com",
        firstName: "Dev",
        lastName: "Eloper",
        team: "React",
        id: 1,
      },
      {
        role: "developer",
        email: "jane@developer.com",
        firstName: "Dev",
        lastName: "Eloper",
        team: "React",
        id: 2,
      },
      {
        role: "user",
        email: "user1@developer.com",
        firstName: "Great",
        lastName: "User",
        isVerified: false,
        id: 3,
      },
      {
        role: "user",
        email: "user2@developer.com",
        firstName: "Super",
        lastName: "User",
        isVerified: false,
        id: 4,
      },
    ];

    const userMap = people.reduce((acc, person) => {
      acc[person.id] = { ...person };
      return acc;
    }, {} as PersonMap);
  });
});

Exercise 2

Update the record.ts file to create a new object type in which the keys are the IDs of the users and the values are the User type.

Currently, the PersonMap type is unknown. Which utility type can we use here together with the Person type to create the appropriate PersonMap type?

Our PersonMap should look like this:

const data: PersonMap = {
    1: {
        role: ...
        email: ...
        firstName: ...
        ...
    }
}

Hint: Remember to use the syntax Person["id"] to access the type of the id property directly from the Person interface.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 2

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/utilities/record.ts to be:

export type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

export type PersonMap = Record<Person["id"], Person>;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 3: Construct a new type by extracting from another type

Using Extract<Type, Union>

Extracts from Type all types that are assignable to Union. It effectively filters out types from Type that do not fit into Union. This utility is particularly useful when you want to create a type based on a subset of another type’s possibilities that meet certain criteria.

Suppose you have a union type that represents various kinds of identifiers in your application:

type ID = string | number | boolean;

type StringOrNumberID = Extract<ID, string | number>;

In the code above, StringOrNumberID ends up being the union of string and number.

So why would you not simply write a union for StringOrNumberID?

Extract shines when used to find the intersection of two different types. See this example:

type Adult = {
  firstName: string;
  lastName: string;
  married: boolean;
  numberOfKids?: number;
}

type Kid = {
  firstName: string;
  lastName: string;
  interests: string[];
  pottyTrained: boolean;
}

type PersonKeys = Extract<keyof Kid, keyof Adult> 
//   ^? "firstName" | "lastName"

In the code above, PersonKeys is the keys that both Kid and Adult have in common, which are firstName and lastName.

Setup 3

✏️ Create src/utilities/extract.ts and update it to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

export type Developer = unknown;

Verify 3

✏️ Create src/utilities/extract.test.ts and update it to be:

import {
  Developer
} from "./extract";
import { describe, it } from "node:test";

describe("extract tests", () => {
  it("typing works", () => {
    const newDev: Developer = {
      role: "developer",
      email: "email@developer.com",
      firstName: "Dev",
      lastName: "Eloper",
      team: "React",
      id: 4,
      // @ts-expect-error
      isVerified: true,
    };
  });
});

Exercise 3

Update the extract.ts file to use the utility type extract on the existing Person type. Extract one of the two possible types from Person to create a new type, Developer.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 3

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/utilities/extract.ts to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

export type Developer = Extract<Person, { role: "developer" }>;

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 4: Construct a new type by excluding types from another type

Using Exclude<Type, ExcludedUnion>

Excludes from Type all types that are assignable to ExcludedUnion. Useful if you want a subset of Type.

type T1 = string | number | boolean;
type T2 = Exclude<T1, boolean>;

const value: T2 = "Hello"; // Works

In the code above, Exclude<T1, boolean> removes boolean from T1, leaving string and number.

Setup 4

✏️ Create src/utilities/exclude.ts and update it to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

type Developer = Extract<Person, { role: "developer" }>;

export interface FrontendDeveloper {
  team: string;
}

Verify 4

✏️ Create src/utilities/exclude.test.ts and update it to be:

import { describe, it } from "node:test";
import { FrontendDeveloper } from "./exclude";

describe("exclude tests", () => {
  it("typing works", () => {
    const brandNewDev: FrontendDeveloper = {
      email: "newHire@developer.com",
      team: "React",
      firstName: "June",
      lastName: "Jones",
      id: 8,
      role: "developer",
    };
    
    const incorrectDev: FrontendDeveloper = {
      email: "newHire@developer.com",
      // @ts-expect-error
      team: "backend",
      firstName: "June",
      lastName: "Jones",
      id: 8,
      role: "developer",
    };
  });
});

Exercise 4

Update the exclude.ts file to create a new type, FrontendDeveloper that excludes the backend value from the team property. Build on the Developer type we previously created.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 4

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/utilities/exclude.ts to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

type Developer = Extract<Person, { role: "developer" }>;

export interface FrontendDeveloper extends Developer {
  team: Exclude<Developer["team"], "backend">;
}

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 5: Include and exclude specific properties

Using Pick<Type, Keys>

Creates a type by picking the set of properties Keys from Type. Useful if you want a subset of Type.

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

type LesserDinosaur = Pick<Dinosaur, "species" | "age">;

const lesserDino: LesserDinosaur = {
  species: "Tyrannosaurus rex",
  age: 27,
};

In the code above, if there is an attempt to add diet to lesserDino then TypeScript will throw an error. Object literals may only specify known properties, and diet does not exist in type LesserDinosaur.

Using Omit<Type, Keys>

Creates a type by omitting the set of properties Keys from Type. Useful if you want a subset of Type.

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

type LesserDinosaur = Omit<Dinosaur, "species" | "age">;

const lesserDino: LesserDinosaur = {
  diet: Diet.Carnivore,
};

lesserDino.species = "Tyrannosaurus rex";

In the code above, if there is an attempt to add species to lesserDino then TypeScript will throw an error. Property species does not exist on type LesserDinosaur. Both species and age key properties are gone!

Setup 5

✏️ Create src/utilities/include-exclude-properties.ts and update it to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

type Developer = Extract<Person, { role: "developer" }>;

interface FrontendDeveloper extends Developer {
  team: Exclude<Developer["team"], "backend">;
}

export interface AdminDeveloper extends FrontendDeveloper {}

Verify 5

✏️ Create src/utilities/include-exclude-properties.test.ts and update it to be:

import { AdminDeveloper } from "./include-exclude-properties";
import { describe, it } from "node:test";

describe("include-exclude-properties tests", () => {
  it("typing works", () => {
    const myAdmin: AdminDeveloper = {
      permissions: ["readData", "writeData"],
      email: "admin@developer.com",
      team: "React",
      firstName: "Admin",
      lastName: "Jones",
      id: 8,
    };

    // @ts-expect-error
    myAdmin.role;
  });
});

Exercise 5

Update the include-exclude-properties.ts file to expand on the implementation of FrontendDeveloper to create a new type, AdminDeveloper where the role property should be replaced by a permissions array.

Have issues with your local setup? You can use either StackBlitz or CodeSandbox to do this exercise in an online code editor.

Solution 5

If you’ve implemented the solution correctly, the tests will pass when you run npm run test!

Click to see the solution

✏️ Update src/utilities/include-exclude-properties.ts to be:

type Person =
  | {
      role: "developer";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      team: "React" | "Angular" | "backend";
    }
  | {
      role: "user";
      email: string;
      id: number;
      firstName: string;
      lastName: string;
      isVerified: boolean;
    };

type Developer = Extract<Person, { role: "developer" }>;

interface FrontendDeveloper extends Developer {
  team: Exclude<Developer["team"], "backend">;
}

export interface AdminDeveloper extends Omit<FrontendDeveloper, "role"> {
  permissions: string[];
}

Have issues with your local setup? See the solution in StackBlitz or CodeSandbox.

Objective 6: Function utility types

Using ReturnType<Type>

Gets the return type of a function Type.

enum Diet {
  "Carnivore",
  "Herbivore",
  "Omnivore",
};

interface Dinosaur {
  species: string;
  diet: Diet;
  age?: number;
};

declare function getDinosaur(): Dinosaur;

type D1 = ReturnType<typeof getDinosaur>;

type D2 = ReturnType<() => Dinosaur>;

In the code above, D1 and D2 are both types Dinosaur.

Next steps

There are other built-in utility types:

If you would like to dive deeper into them, check the official documentation for TypeScript.