🎁 Give the #1 gift request of 2024... a ZTM membership gift card! 🎁

TypeScript Utility Types: A Beginners Guide (With Code Examples)

Jayson Lennon
Jayson Lennon
hero image

Are you a TypeScript user who wants to get more out of TypeScript?

Well, good news! In this guide, we'll explore some of the more commonly used TypeScript utility types, and how they work - with code examples.

Why care about these?

Simply because, Utility types can manipulate the characteristics of existing types to create new ones with different properties.

This can then help streamline development of your current and future programs, by reducing the burden of maintaining multiple similar types.

As you can imagine, this is pretty handy!

utility types

Sidenote: If you're struggling to understand Utility types and how to use them, want to learn more, or simply just want a deeper dive into TypeScript, then check out my complete course on Typescript here.

learn typescript

It takes you from a complete beginner to building real-world applications and being able to get hired, so there's something for every level of TypeScript user.

Check it out above, or watch the first videos here for free.

With that out of the way, let's get into Utility types in TypeScript and how they work...

Partial

The Partial type takes all the properties of a type, and then makes a new type that is update-compatible with the original type.

This allows you to use Partial to change the properties of the original type using the spread operator.

For example

We could use this interface for products in a store, like so:

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
}

And then we could write a few functions using Partial:

// update existing products
function updateProduct(product: Product, update: Partial<Product>) {
  Object.assign(product, update);
}

// create new products from existing ones
function newProduct(existing: Product, newData: Partial<Product>): Product {
  // Use spread (...) to take the properties from existing objects and compose
  // them together into a new object.
  return { ...existing, ...newData, id: existing.id += 1 };
}

Simple!

And since we used Partial as a function parameter, we can now construct arbitrary objects having at least 1 property from the original object.

This now enables us to compose existing products:

const shoe = {
  id: 1,
  name: "Shoe",
  description: "Standard quality",
  price: 39.99,
};

updateProduct(shoe, { description: "Heavy duty" });
// { id: 1, name: 'Shoe', description: 'Heavy duty', price: 39.99 }

const boot = newProduct(shoe, { name: "Boot" });
// { id: 2, name: 'Boot', description: 'Heavy duty', price: 39.99 }

Pretty handy right?

So let's get into the next option.

Required

The Required type forces all optional properties in an object to be required.

For example

This can be useful when creating a user login and registration process.

Why?

Well, during registration you'll typically collect more information, and the user will be required to fill out the entire form. However, during login you'll only need the user's login name and password.

This is because both the login form and registration form can now share the same interface by using Required:

interface UserForm {
  email: string;
  password: string;
  // optional
  address?: string;
  // optional
  phoneNumber?: string;
}

// the login form keeps the optional fields declared above
type UserLogin = UserForm;
const login: UserLogin = {
  email: "user@example.com",
  password: "123",
};

// the registration form requires all of the information
type UserRegistration = Required<UserForm>;
const register: UserRegistration = {
  email: "user@example.com",
  password: "123",
  address: "The internet",
  phoneNumber: "555-555-5555",
};

Now whenever we write code using Required<UserForm>, TypeScript will ensure that all the properties are available, so we don't have to worry about checking for the existence of the optional fields.

required utility type

Pick

The Pick utility type allows you to select some properties from a type, and then use only those properties in your new type.

For example

Let's say that we have a Shippable interface for tracking packages across a distribution center. Some parts of the process will require the dimensions and weight of the package, and others will require the destination.

Using Pick enables us to pick just those properties needed at the given points in the process:

interface Shippable {
  weight: number;
  dimensions: [number, number, number];
  destination: string;
}

// only pick the `destination` property
function routeToDestination(item: Pick<Shippable, "destination">) { }

// pick both the `weight` and `dimensions` properties
function calculateFee(item: Pick<Shippable, "weight" | "dimensions">) { }

const box: Shippable = {
  weight: 30,
  dimensions: [5, 5, 5],
  destination: "somewhere",
};

calculateFee(box);
routeToDestination(box);

Reducing the amount of data that traverses the system helps keep programs more maintainable and easier to reason about.

Omit

The opposite of Pick, the Omit type will take all properties of an object, except the ones indicated.

This can be useful when you have many properties that you need from a type, but a few extra that you don't need.

For example

You can use Omit to remove database IDs from an API response:

interface Entity {
  id: number;
  firstName: string;
  lastName: string;
  age: number;
}

type EntityResponse = Omit<Entity, "id">;

// Using EntityResponse prevents us from
// accidentally sending the database id.
const bob: EntityResponse = {
  firstName: "Bob",
  lastName: "E.",
  age: 27,
};

Omit works with union types as well, so if you only wanted the first and last name from the Entity, you could do so like this:

type EntityResponse = Omit<Entity, "id" | "age">;

Extract and Exclude

Extract is similar to Pick, except it works on unions of types instead of properties.

This means that it allows certain types to be included in the new type, but only if they meet a certain criteria.

For example

If we create a type to contain information about departments for a business, and the people who belong to those departments, we can use a union like this:

type Departments =
  | { name: "Customer Service"; category: "client", people: Employee[] }
  | { name: "Executive"; category: "leadership", people: Member[] }
  | { name: "Human Resources"; category: "employee", people: Employee[] }
  | { name: "IT Operations"; category: "IT", people: Employee[] }
  | { name: "IT Support"; category: "IT", people: Employee[] }
  | { name: "Maintenance"; category: "building services", people: Employee[] }
  | { name: "Manufacturing"; category: "products", people: Employee[] }
  | { name: "Quality Assurance"; category: "business internal", people: Employee[] }
  | { name: "Research & Development"; category: "products", people: Employee[] }
  | { name: "Software Engineering"; category: "IT", people: Employee[] }

As you can see, there are a lot of departments here!

However, what will often be the case is that we only want to work with a subset of the departments. And so by using Extract, we can get only the departments in the IT category:

type TechnologyDepartments = Extract<Departments, { category: "IT" }>;
// this type is a new union type that has only these from
// the original union:
//   | { name: "IT Operations"; category: "IT", people: Employee[] }
//   | { name: "IT Support"; category: "IT", people: Employee[] }
//   | { name: "Software Engineering"; category: "IT", people: Employee[] }

Simple!

Exclude

Exclude is the opposite of Extract, in that if we don't want some of the types, we can choose to exclude them instead:

type NonTechnologyDepartments = Exclude<Departments, { category: "IT" }>;

NonTechnologyDepartments will consist of all types in the union except the ones categorized with "IT".

Record

The Record type provides a way to specify what properties are allowed in an object.

It's typically useful when you know ahead of time what properties you should have (such as when dealing with an external API) and what their types will be.

Encoding this information using a Record provides more type safety because TypeScript will ensure that:

  • All the properties have the correct type
  • All the properties exist, and
  • There are no extra properties

For example

Let's assume we're running a website to show performance benchmark results for various programming languages.

Whenever we finish running a benchmark suite, we want to make sure we always upload results for every language we are testing. This way, the web site won't have any gaps between languages.

First we need to encode all of the languages we plan on having benchmark tests for, as these will be the keys when we create our Record.

We'll also create an interface for the benchmark results, like so:

// We only support these languages:
type Languages = "typescript" | "python" | "java";

// Each language has results for these two benchmarks:
interface Result {
  sort: number;
  serveHttp: number;
}

Now we can create a Record containing the benchmark results from the test run:

const benchmarkResults: Record<Languages, Result> = {
  typescript: { sort: 4, serveHttp: 7 },
  python: { sort: 3, serveHttp: 5 },
  java: { sort: 8, serveHttp: 8 },
  // ERROR: doesn't exist in `Languages` union
  // php: { sort: 5, serveHttp: 8} 
};

Since we opted to use a Record, TypeScript requires the typescript, python, and java properties to exist. If we forget a property, or add an extra property, we'll get a compiler error.

This is exactly what we want because we won't be able to submit results if we don't have every programming language present. Then, if we later decide to include a new programming language, we only need to update the Languages union.

This is because once it's added to the union, TypeScript will produce compilation errors at all locations in our code where we need to add support for the new language, making it very hard (and sometimes impossible) to introduce a bug when making this kind of change to the code.

Smart eh!?

Parameters and ConstructorParameters

The Parameters type makes it easier to work with functions that have many parameters. It does this by looking at all the parameters that exist on a function and creating a tuple type from them, which you can then use to build up an argument list.

After building up the argument list in the tuple, you can then pass it around anywhere in the program and still maintain the ability to call the originating function, since all the arguments are contained in a single type.

You could also duplicate or make changes to the data in the tuple, making it easy to call the function with minor changes in only 1 or 2 of the arguments.

For example:

type Callback = (result: string) => void;

// Lots of params in this function...
function fetchData(
  url: string,
  method: 'GET' | 'POST' | 'PUT',
  headers: Record<string, string>,
  body: Record<string, any> | FormData | null,
  timeout: number,
  callback: Callback
): void { }

// Make a type containing the parameters of the `fetchData` function
type FetchParams = Parameters<typeof fetchData>;

// This is now type-checked when using the annotation:
const params: FetchParams = [
  "https://example.com/",
  "GET",
  { "Content-type": "application/json" },
  {},
  5000,
  (data) => { console.log("response: ${data}") },
];

// Now that we have all the data we need to call
// the function, we can do so with the spread operator:
fetchData(...params);

ConstructorParameters

Finally, we have ConstructorParameters. These are just like Parameters, except they operate on the constructor of a class instead of a regular function.

For example

This would allow you to instantiate multiple objects from a single tuple.

This could be great when you only need to make a minor change between each object, such as instantiating multiple graphical pins on a map at different locations.

Boom!

What Utility Types will you add to your own code?

So there you have it - 9 of the most commonly used Utility Types in TypeScript, and how to use them.

Hopefully this guide has helped clear up some confusion around Utility types, while also giving you a few ideas of how you might use them in your own projects!

Utility Types are incredibly helpful for streamlining your code, so make sure you give these a try.

And like I said up top. If you're struggling to grasp some of the concepts around Utility types, how to apply them, or just want a deeper dive into TypeScript, then check out my complete course on Typescript here.

Or come watch the first few lectures here for free.

The best part? When you join, you'll have direct access to me, other students, as well as full-time TypeScript devs in our private Discord channel - so you'll never be stuck!

BONUS: More TypeScript tutorials, guides & resources

If you've made it this far, you're clearly interested in TypeScript so definitely check out all of my TypeScript posts and content:

More from Zero To Mastery

Type Checking In TypeScript: A Beginners Guide preview
Type Checking In TypeScript: A Beginners Guide

What if you could catch errors at both compile and runtime? Thanks to TypeScript's type checking feature, you can! Learn how in this guide, with code examples.

TypeScript Union Types: A Beginners Guide preview
TypeScript Union Types: A Beginners Guide

Just what are Union Types in TypeScript? How do they work, what are they for, and how do you use them? Learn all this and more, with code examples ⚡

How To Ace The Coding Interview preview
How To Ace The Coding Interview

Are you ready to apply for & land a coding job but not sure how? This coding interview guide will show you how it works, how to study, what to learn & more!