Back

TypeScript Utility Types from Scratch

MD Rashid Hussain
MD Rashid Hussain
Oct-2024  -  5 minutes to read
Image source attributed to: https://microsoft.com

Typescript utility types are a set of predefined generic types that can be used to perform common operations on types. They are very useful when working with types in Typescript. In this article, we will explore some of the most commonly used utility types in Typescript.

In this article, we'll experiment and explore on how to implement some of the most commonly used utility types in Typescript from scratch.

Before all of that, let's add some baseline for testing our implementation, so that you can also setup your own testing/playground environment.

// to assert that our type is correct
export type Expect<T extends true> = T;
 
// to assert that the two parameters passed (X, Y) are equal in type
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? true
  : false;

Don't worry about this piece of code, it's just a helper type to make the test cases more readable and easier to write. Later on, while checking your implementation, you can check them by using the Expect and Equal type.

type ActualType = 'actual'; // any type
type ExpectedType = 'expected'; // any type
 
type tests = [
	Expect<Equal<ExpectedType, ActualType>>; // type signature
	Expect<Equal<Partial<User>, MyPartial<User>>>; // example for Partial
]

If the test passes, there will be no errors in the code. Else, you'll have red squiggly lines in your code editor (we are using VSCode here). Let's start with the implementations.

The Partial utility type can be used to make all properties of a type optional. This is very useful when you get certain data from the API and you want to make sure you handle the case where some properties may be missing.

type User = { name: string; age: number };
type PartialUser = Partial<User>; // equivalent to: { name?: string; age?: number }
 
/**
 * Implementation
 * Make all key optional can be achieved by using the `?` modifier
 */
type MyPartial<T> = { [P in keyof T]?: T[P] };

The Required utility type can be used to make all properties of a type required.

type PartialUser = { name?: string; age?: number };
type User = Required<PartialUser>; // equivalent to: { name: string; age: number }
 
/**
 * Implementation
 * Make all key required can be achieved by using the `-?` modifier
 */
type Required<T> = { [P in keyof T]-?: T[P] };

The Readonly utility type can be used to make all properties of a type readonly.

type User = { name: string; age: number };
type ReadonlyUser = Readonly<User>; // equivalent to: { readonly name: string; readonly age: number }
 
/**
 * Implementation
 * Make all key readonly can be achieved by using the `readonly` modifier
 */
type Readonly<T> = { readonly [P in keyof T]: T[P] };

The Record utility type can be used to create a type with a set of properties of a given type.

type User = { name: string; age: number };
type UserRecordKv = Record<string, User>; // equivalent to: { 'one': User, 'two': User, ... }
 
/**
 * Implementation
 * Create a type with a set of properties of a given type can be achieved by using the `Record` type
 */
type Record<K extends keyof any, T> = { [P in K]: T };

The Pick utility type can be used to create a type by picking some properties from another type.

type User = { name: string; age: number; email: string };
type UserPick = Pick<User, 'name' | 'age'>; // equivalent to: { name: string; age: number }
 
/**
 * Implementation
 * Create a type by picking some properties from another type can be achieved by using the `Pick` type
 */
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

The Omit utility type can be used to create a type by omitting some properties from another type.

type User = { name: string; age: number; email: string };
type UserOmit = Omit<User, 'email'>; // equivalent to: { name: string; age: number }
 
/**
 * Implementation
 * Create a type by omitting some properties from another type can be achieved by using the `Omit` type
 */
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; // using Pick and Exclude

The Awaited utility type can be used to get the type of the value that a promise resolves to.

type User = Promise<{ name: string; age: number }>;
type AwaitedUser = Awaited<User>; // equivalent to: { name: string; age: number }
 
/**
 * Implementation
 * Get the type of the value that a promise resolves to can be achieved by using the `Awaited` type
 */
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
  ? U extends PromiseLike<any>
    ? MyAwaited<U>
    : U
  : never;

The Exclude utility type can be used to exclude some types from another type.

type User = { name: string; age: number };
type UserExclude = Exclude<keyof User, 'age'>; // equivalent to: 'name'
 
/**
 * Implementation
 * Exclude some types from another type can be achieved by using the `Exclude` type
 */
type MyExclude<T, U> = T extends U ? never : T;

The ReturnType utility type can be used to get the return type of a function type.

type User = () => { name: string; age: number };
type UserReturnType = ReturnType<User>; // equivalent to: { name: string; age: number }
 
/**
 * Implementation
 * Get the return type of a function type can be achieved by using the `ReturnType` type
 */
type MyReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer U
  ? U
  : never;

The Parameters utility type can be used to get the parameter types of a function type.

type User = (name: string, age: number) => void;
type UserParameters = Parameters<User>; // equivalent to: [string, number]
 
/**
 * Implementation
 * Get the parameter types of a function type can be achieved by using the `Parameters` type
 */
type MyParameters<T extends (...args: any[]) => any> = T extends (
  ...args: infer S
) => any
  ? S
  : never;

The Extract utility type can be used to extract some types from another type.

type User = { name: string; age: number };
type UserExtract = Extract<keyof User, 'age'>; // equivalent to: 'age'
 
/**
 * Implementation
 * Extract some types from another type can be achieved by using the `Extract` type
 */
type MyExtract<T, U> = T extends U ? T : never;

The NonNullable utility type can be used to exclude null and undefined from a type.

type User = { name: string; age: number | null };
type UserNonNullable = NonNullable<User['age']>; // equivalent to: number
 
/**
 * Implementation
 * Exclude `null` and `undefined` from a type can be achieved by using the `NonNullable` type
 */
type MyNonNullable<T> = T extends null | undefined ? never : T;

In this article, we explored some of the most commonly used utility types in Typescript and implemented them from scratch. We also added some test cases to verify our implementation. I hope you found this article helpful and learned something new. If you have any questions or feedback, feel free to leave a comment below.