Roy Lopez
PersistDev.blog
#TypeScript

Exploring TypeScript Utility Types for Cleaner, More Flexible Code

Exploring TypeScript Utility Types for Cleaner, More Flexible Code
0 views
7 min read
#TypeScript

TypeScript provides a powerful set of utility types that streamline common transformations of types, saving time and enhancing code readability. Utility types are especially useful for manipulating types without needing to rewrite them, making your code more flexible and type-safe. This article explores some of the most widely used TypeScript utility types, complete with examples and practical use cases.

What Are TypeScript Utility Types?

Utility types are built-in types that allow you to transform and compose existing types. Instead of manually creating new types by altering properties or structures, utility types let you apply modifications like making properties optional, picking or omitting properties, and more—all with a concise syntax.

Commonly Used TypeScript Utility Types

1. Partial<T>

The Partial utility type makes all properties in a type optional. This is helpful when you’re working with objects that may not have all properties defined initially, such as during data updates or partial updates in forms.

type Person = {
  name: string;
  age: number;
  email: string;
};

type PartialPerson = Partial<Person>; // { name?: string; age?: number; email?: string }

Use Case:
Imagine a function that updates a Person object. Using Partial<Person>, you allow any subset of the properties to be updated, rather than requiring a full Person object:

function updatePerson(id: number, updates: Partial<Person>) {
  // Update logic here
}

updatePerson(1, { name: "Alice" }); // Only updating the name
updatePerson(2, { age: 30, email: "alice@example.com" }); // Updating age and email

2. Required<T>

The Required utility type does the opposite of Partial: it makes all properties in a type required. This is useful if you’re working with a type where some properties might have been optional but are required in certain contexts.

type OptionalPerson = {
  name?: string;
  age?: number;
};

type FullPerson = Required<OptionalPerson>; // { name: string; age: number }

Use Case:
When dealing with optional properties, Required ensures all properties are present before performing certain operations, preventing runtime errors due to missing values.

3. Readonly<T>

The Readonly utility type makes all properties in a type immutable, meaning they cannot be reassigned once set. This is ideal for working with constant objects or data structures where changes should be restricted.

type Person = {
  name: string;
  age: number;
};

const person: Readonly<Person> = { name: "Bob", age: 25 };

// person.age = 26; // Error: Cannot assign to 'age' because it is a read-only property

Use Case:
In situations where you want to prevent accidental mutation of objects—such as configuration objects or default settings—Readonly can be a great fit.

4. Record<K, T>

The Record utility type creates an object type with specific keys of type K and values of type T. It’s useful for defining objects with fixed keys that all share the same value type.

type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, boolean>;

const permissions: Permissions = {
  admin: true,
  user: false,
  guest: false,
};

Use Case:
When you need an object with a specific set of keys (e.g., roles, categories) and consistent value types, Record provides a clean solution.

5. Pick<T, K>

The Pick utility type creates a new type by selecting a subset of properties from an existing type. This is ideal when you want to narrow down a larger type to a specific set of properties.

type Person = {
  name: string;
  age: number;
  email: string;
};

type NameAndEmail = Pick<Person, "name" | "email">; // { name: string; email: string }

Use Case:
When only certain properties are relevant to a specific operation, Pick allows you to create a simplified type focused on just those properties.

6. Omit<T, K>

The Omit utility type is the opposite of Pick: it creates a type by removing specific properties from an existing type. This is useful when you need most, but not all, of the properties from a type.

type Person = {
  name: string;
  age: number;
  email: string;
};

type PersonWithoutEmail = Omit<Person, "email">; // { name: string; age: number }

Use Case:
In situations where sensitive or irrelevant information needs to be excluded, Omit allows you to remove properties that should be left out.

7. Exclude<T, U>

The Exclude utility type constructs a type by removing all types in U from T. It’s commonly used to refine union types.

type AllTypes = string | number | boolean;
type StringOrBoolean = Exclude<AllTypes, number>; // string | boolean

Use Case:
When working with union types, Exclude lets you remove certain options that aren’t needed in a specific scenario, making your types more precise.

8. Extract<T, U>

The Extract utility type is the opposite of Exclude: it constructs a type by extracting only the types in U from T. This is handy for narrowing down a type to a specific subset of union members.

type AllTypes = string | number | boolean;
type OnlyString = Extract<AllTypes, string>; // string

Use Case:
Use Extract to focus on specific members of a union type without redefining the entire type.

9. NonNullable<T>

The NonNullable utility type removes null and undefined from a type. This is useful for ensuring that a type doesn’t allow null or undefined values.

type NullableString = string | null | undefined;
type NonNullString = NonNullable<NullableString>; // string

Use Case:
When working with optional or nullable properties, NonNullable helps enforce non-nullable constraints for safer type handling.

10. ReturnType<T>

The ReturnType utility type extracts the return type of a function. This is useful when you need to know the return type of a function without calling it directly.

function getPerson() {
  return { name: "Alice", age: 30 };
}

type PersonType = ReturnType<typeof getPerson>; // { name: string; age: number }

Use Case:
ReturnType is valuable when working with dynamically created functions or when you want to ensure the correct return type is maintained across the code.

11. InstanceType<T>

The InstanceType utility type extracts the instance type of a constructor function or class. This is helpful when you need a type that represents an instance of a class without instantiating it.

class Person {
  constructor(
    public name: string,
    public age: number,
  ) {}
}

type PersonInstance = InstanceType<typeof Person>; // Person

Use Case:
Use InstanceType when you need to work with class instances or share types across various parts of your code that rely on instantiated objects.

Summary

TypeScript’s utility types make it easier to work with existing types and create new ones without redundant code. Here’s a quick recap:

  • Partial<T>: Makes all properties optional.
  • Required<T>: Makes all properties required.
  • Readonly<T>: Makes all properties immutable.
  • Record<K, T>: Creates a type with specific keys and consistent value types.
  • Pick<T, K>: Selects a subset of properties from a type.
  • Omit<T, K>: Removes specified properties from a type.
  • Exclude<T, U>: Excludes specific types from a union.
  • Extract<T, U>: Extracts specific types from a union.
  • NonNullable<T>: Removes null and undefined from a type.
  • ReturnType<T>: Gets the return type of a function.
  • InstanceType<T>: Gets the instance type of a class.

By mastering utility types, you’ll have a powerful toolset for handling complex type scenarios in TypeScript, reducing the need for repetitive and boilerplate code, and ensuring stronger type safety in your projects.

Loading...