Common utility types in Typescript and their usages

Common utility types in Typescript and their usages

·

6 min read

In Typescript projects, instead of declaring all types manually, we can leverage utility types to transform existing types into new ones, particularly when these types share similar properties. This approach not only helps adhere to the DRY (Don’t repeat yourself) principle but also enhance code readability and maintainability. In this article, we will delve into some of the most common utility types and explore their real-world use cases.

1. ReturnType

The ReturnType<Type> utility, as its name suggests, extracts the return type of a function Type. This utility is especially useful when we want to infer the type of the value that a function returns.

Basic example:

type T = ReturnType<() => number>;
// type T = number;

Real-world use case:

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

type ReturnSum = ReturnType<typeof sum>;
// type ReturnSum = number

Special cases to note:

  • Generic functions
type T = ReturnType<<Type>() => Type>;
// Equivalent to:
type T = unknown;
  • Any
type T = ReturnType<any>;
// Equivalent to:
type T = any;
  • Never
type T = ReturnType<never>;
// Equivalent to:
type T = never;
  • Other types that do not satisfy the constraint (...args: any) => any
type T1 = ReturnType<string>;
type T2 = ReturnType<Function>;
// Equivalent to:
type T = any;

2. Awaited

The Awaited<Type> utility is used to extract the type resolved by a Promise. It’s particularly useful for handling asynchronous code and increases the code readability.

Basic example:

type T = Awaited<Promise<String>> 
// type T = string

Real-world use case:

async function fetchData(url: string): Promise<String> {
      const response = await fetch(urt);
      const result = await response.json();
      return result;
}
type Data = Awated<ReturnType>;
// type Data = string

3. Partial

The Partial<Type> utility is used to transform an existing Type into a new type where all properties of Type are optional. This utility is especially useful when working with API integrations or scenarios where a subset of properties may be provided.

Real-world use case:

type Post = {
    id: number,
    title: string,
    description: string,
}
async function fetchPosts(url: string): Promise<Post[]> {
    const res = await fetch(url);
    const posts = await res.json();
    return posts;
};
async function updatePost(url: string, id: number, data: Partial<Post>) {
    const res = await fetch(url, {
                           method: "PATCH",
                           body: JSON.stringify(data)
                      })
    return res.status;
}
// When updating a post, we do not need to send all post properties to sever
updatePost('example.com',1, { title: 'Updated title'})

4. Readonly

The Readonly<Type> utility makes all properties of an object Type immutable. This is particularly useful for ensuring that certain data should remain unchanged throughout our application.

Real-world use case:

type Config = {
    url: string,
    timeout: number
}

const config: ReadOnly<Config> = {
    url: 'https://example.com',
    timeout: 5000
}

person.url = 'https://app.example.com';
// Error: Cannot assign to 'url' because it is a read-only property

Note that this utility only works for top-level properties, it does not affect nested objects:

type Person = {
    name: string,
    address: {
        city: string,
    }
}
type T = ReadOnly<Person>;
const person: T = {
    name: 'John',
    address: {
        city: 'New York'
    }
}

// This reassignment is valid 
person.address.city = 'LA';

5. Record

The Record<Key,Type> utility is used to define the type of an object where type of the keys is Key and type of the values is Type.

type T = Record<string, number>;
const score: T = {
    'Alice': 10,
    'John': 9,
    'Chloe': 8,
}

This utility is useful for defining an object type where its keys map to specific value types:

type Role = 'admin' | 'editor' | 'viewer'
type Permission = 'read' | 'write' | 'delete';

type T = Record<Role, Permission[]>;

const permissions: T = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

6. Pick

The Pick<Type, Keys> is used to extract a subset of properties from Type based on specific keys Keys. It helps reduce code repetition and improve maintainability.

Real-world use case:

type Customer = {
    name: string,
    age: number,
    phone: string,
    email: string,
}
type Contact = Pick<Customer, 'telephone' | 'email'>;
function getCustomerContact(user: Customer): Contact {
    return ({
        phone: user.phone,
        email: user.email
    })
}
// Contact is equivalent to:
type T = {
    telephone: string,
    email: string,
}

7. Omit

The Omit<Type, Keys> utility is the opposite of the Pick<Type, Keys> utility. It creates the new object type by copying all properties of Type except for the specified properties in Keys.

Real-world use case:

type User = {
  id: number;
  name: string;
  email: string;
  password: string;
};
type UserData = Omit<User, 'password'>;
function serializeUser(user: User): UserData {
  const { password, ...userData } = user;
  return userData;
}
// UserData equivalent to
type T = {
     id: number;
     name: string;
     email: string;
}

8. Template literal types

Inspired by template literal strings in Javascript, we can use template literals to create new types, including string literal types (a type representing specific string values instead of the general string type). Let’s break down the example below:

type Studen = 'student'; // this is a string literal type
type T = `${Student}Email` 
// Equivalent to:
type T = 'studentEmail';

When combined with union types, we can flexibly create a union type that represents a set of all possible string literal types, as shown below:

type Role = 'student' | 'staff';
type Contact = 'phone' | 'email';
type T = `${Role}_${Contact}`;
// Equivalent to:
type T = 'student_phone' | 'student_email' | 'staff_phone' | 'staff_email';

For easier string manipulation, Typescript provides a set of utilities to modify string literal types. These utilities include:

Uppercase<StringType>: Transforms all characters in the string literal to the uppercase version.

type Student =  'student';
type T = Uppercase<Student>; 
// type T = 'STUDENT'

// Used in creating template literal type
type T = `${Uppercase<Student>}_ID`; 
// type T = 'STUDENT_ID';

// Used in generic type
type Contact<Str extends string> = `${Uppercase<Str>}_CONTACT`;
type T = Contact<Student>; 
// T = `STUDENT_CONTACT`;

Lowercase<StringType>: Transforms all characters in the string literal to the lowercase version.

type Student = 'STUDENT';
type T = Lowercase<Student>; 
// T = 'student';

Capitalize<StringType>: Transforms the first character in the string literal to the uppercase version.

type Student = 'student';
type T = Capitalize<Student>; 
// T = 'Student';

Uncapitalize<StringType>: Transforms the first character in the string literal to the lowercase version.

type Student = 'STUDENT';
type T = Uncapitalize<Student>; 
// T = 'sTUDENT';

Note that all string-type utilities mentioned above can be used with union types, but each type in a union type must be a string literal type. Otherwise, Typescript will throw an error: Type '...' is not assignable to type 'string'

type Role = 'student' | 'staff';
type T = Capitalize<Role>;
// type T = 'Student' | 'Staff'

// Invalid union type for string literal
type Score = 'A' | 'B' | 1;
type T = Lowercase<Score>
// Error: Type 'number' is not assignable to type 'string'

That covers the most common and useful utility types from my perspective. If you’d like to explore full type utility types provided by Typescript, feel free to visit the official: TypeScript: Documentation - Utility Types.