Learn TypeScript With Me in 14 Days Day 10

Posted on 10 October 2024 Reading time: 7 min read
Learn TypeScript With Me in 14 Days Day 10

Day 10: Utility Types in TypeScript

In this part of the series, we will learn about Utility Types in TypeScript, which are built-in types that simplify many common type transformations. These utilities save us from writing repetitive code by allowing us to quickly create new types based on existing ones. By the end of this session, we will be familiar with the most commonly used utility types and how to use them to streamline the Task Manager project.

Why Use Utility Types?

Utility types are built-in helpers provided by TypeScript to make type manipulation easier. They allow us to modify and transform existing types without having to manually redefine them. Whether we need to make properties optional, read-only, or pick and omit specific properties, utility types provide a clean and efficient way to work with types in our project.

Common Utility Types

Let’s explore some of the most commonly used utility types in TypeScript.

1. Partial

The Partial utility allows you to create a type where all properties of the original type are optional. This is useful when you don’t want to provide all properties when updating or initialising objects.

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

function updateTask(id: number, updates: Partial<Task>): Task {
  const task = { id, title: 'Default Title', completed: false }; // Example task
  return { ...task, ...updates };
}

const updatedTask = updateTask(1, { title: 'Updated Title' });
console.log(updatedTask);  // Output: { id: 1, title: 'Updated Title', completed: false }

Explanation:

  • Partial: The Partial utility creates a type where all properties of Task are optional.
  • Use Case: This is useful when updating only specific properties of an object without having to provide all properties.

2. Required

The Required utility makes all properties of a type mandatory, overriding any optional properties in the original type.

interface Task {
  id: number;
  title?: string;
  completed: boolean;
}

const task: Required<Task> = { id: 1, title: 'Complete project', completed: true };
console.log(task);

Explanation:

  • Required: This utility ensures that all properties, even optional ones, are required.
  • Use Case: Use this when you need all properties to be present, such as when dealing with data that must be fully populated.

3. Readonly

The Readonly utility makes all properties of a type immutable, meaning they cannot be reassigned.

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

const task: Readonly<Task> = { id: 1, title: 'Learn TypeScript', completed: false };
// task.title = 'New Title';  // Error: Cannot assign to 'title' because it is a read-only property

Explanation:

  • Readonly: This utility ensures that no properties of the task can be modified after initialisation.
  • Use Case: Useful for scenarios where immutability is important, such as managing state or ensuring certain values don’t change after being set.

4. Pick<Type, Keys>

The Pick<Type, Keys> utility allows you to create a type that includes only the specified keys from the original type.

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

type TaskOverview = Pick<Task, 'id' | 'title'>;

const taskOverview: TaskOverview = { id: 1, title: 'Complete project' };
console.log(taskOverview);

Explanation:

  • Pick<Task, ‘id’ | ‘title’>: This creates a new type that only includes the id and title properties from Task.
  • Use Case: Use this when you only need specific properties from a larger object type.

5. Omit<Type, Keys>

The Omit<Type, Keys> utility creates a type that excludes the specified keys from the original type.

interface Task {
  id: number;
  title: string;
  completed: boolean;
}

type TaskWithoutCompletion = Omit<Task, 'completed'>;

const task: TaskWithoutCompletion = { id: 1, title: 'New Task' };
console.log(task);

Explanation:

  • Omit<Task, ‘completed’>: This utility removes the completed property from the Task type.
  • Use Case: Useful when you want to work with a type that excludes certain properties.

Applying Utility Types to Task Manager

Now that we understand the common utility types, let’s apply them to enhance our Task Manager project.

1. Using ‘Partial’ for Task Updates

We can simplify the task update process by using the Partial utility to make all properties of the Task type optional during an update.

import { Task } from './task';

class TaskManager {
  private tasks: Task[] = [];

  updateTask(id: number, updates: Partial<Task>): Task | undefined {
    const task = this.tasks.find(task => task.id === id);
    if (task) {
      Object.assign(task, updates);
    }
    return task;
  }
}

const taskManager = new TaskManager();
taskManager.updateTask(1, { completed: true });

Explanation:

  • Partial allows us to update a task with only the fields we want to modify without requiring all properties.

2. Using ‘Readonly’ for Immutable Tasks

In scenarios where tasks should be immutable after being added to the system, we can use the Readonly utility type.

import { Readonly } from 'typescript';

const readonlyTask: Readonly<Task> = { id: 1, title: 'Immutable Task', completed: false };
// readonlyTask.completed = true;  // Error: Cannot assign to 'completed' because it is a read-only property

Explanation:

  • Readonly ensures that no task properties can be modified after it’s created.

3. Using ‘Pick’` for Task Overview

If we need to display only part of a task’s details (for example, only the id and title), we can use the Pick utility to extract these properties.

type TaskOverview = Pick<Task, 'id' | 'title'>;

const taskOverview: TaskOverview = { id: 1, title: 'Complete project' };
console.log(taskOverview); // Output: { id: 1, title: 'Complete project' }

Explanation:

  • Pick<Task, ‘id’ | ‘title’> allows us to create a lightweight version of the Task type that only includes specific properties.

Other Useful Utility Types

1. Record<Keys, Type>

The Record<Keys, Type> utility creates an object type where the keys are of type Keys and the values are of type Type.

import { Task } from './task';

type TaskRecord = Record<number, Task>;

const tasks: TaskRecord = {
  1: { id: 1, title: 'Learn TypeScript', completed: false },
  2: { id: 2, title: 'Build Task Manager', completed: true },
};

Explanation:

  • Record<number, Task> creates an object where each key is a number, and each value is a Task.

2. Exclude<Type, ExcludedUnion>

The Exclude<Type, ExcludedUnion> utility removes certain types from a union.

type Status = 'pending' | 'completed' | 'archived';
type ActiveStatus = Exclude<Status, 'archived'>;

const taskStatus: ActiveStatus = 'completed'; // Valid
// const archivedTask: ActiveStatus = 'archived'; // Error: Type '"archived"' is not assignable to type 'ActiveStatus'

Explanation:

  • Exclude<Status, ‘archived’> removes the ‘archived’ status, leaving only ‘pending’ and ‘completed’ as valid options.

Example: Using Utility Types for Form Data

We can combine Partial and Readonly to handle both optional fields and immutability. For instance, form fields may be optional during initial input, but once submitted, you want to prevent any modifications.

interface UserForm {
  name: string;
  email: string;
  age: number;
  address: string;
}

// A form that allows optional fields during input
type EditableForm = Partial<UserForm>;

// Once submitted, the form data becomes read-only
type SubmittedForm = Readonly<UserForm>;

// Example usage:
const editableForm: EditableForm = { email: '[email protected]' };
const submittedForm: SubmittedForm = { name: 'John', email: '[email protected]', age: 30, address: '100 London Rd, London' };

// Editable form allows updating fields
editableForm.name = 'Jane Doe';

// Submitted form is read-only and prevents updates
// submittedForm.name = 'Jane Doe'; // Error: Cannot assign to 'name' because it is a read-only property

Explanation:

  • Partial allows for flexibility during the form input stage, as not all fields need to be provided.
  • Readonly locks the form data after submission to ensure no further changes are made.

What’s Next?

In this part of the series, we learnt how to use Utility Types to simplify type transformations and manipulations in TypeScript. We applied utility types such as Partial, Readonl, Pick, and Omit to enhance the Task Manager project. These types are crucial for writing clean, maintainable code that is both flexible and type-safe.

In the next part of the series, we will dive into Decorators in TypeScript, an advanced feature that allows us to add functionality to classes, methods, and properties in a clean and reusable way.

Useful Resources

TypeScript Documentation - Utility Types In TypeScript

Understanding TypeScript Utility Types