Building Your Own Utility Types

Re-creating the Built-ins

Most built-in utilities are a single line of mapped or conditional type. Writing them yourself is a very common interview exercise.

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyOmit<T, K extends keyof any> = { [P in keyof T as P extends K ? never : P]: T[P] };

Deep Versions

Example

Built-in Partial and Readonly are shallow. A recursive mapped type applies them to every level.

type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
type DeepReadonly<T> = T extends (infer E)[]
  ? readonly DeepReadonly<E>[]
  : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;

type Settings = {
  theme: { mode: "light" | "dark"; accent: string };
  notifications: { email: boolean; sms: boolean };
  tags: string[];
};

const defaults: Settings = {
  theme: { mode: "light", accent: "blue" },
  notifications: { email: true, sms: false },
  tags: ["new-user"],
};

function merge(base: Settings, patch: DeepPartial<Settings>): DeepReadonly<Settings> {
  return {
    theme: { ...base.theme, ...patch.theme },
    notifications: { ...base.notifications, ...patch.notifications },
    tags: (patch.tags as string[] | undefined) ?? base.tags,
  };
}

const s = merge(defaults, { theme: { mode: "dark" }, notifications: { sms: true } });
console.log(s.theme, s.notifications, s.tags);

Output

{ mode: 'dark', accent: 'blue' } { email: true, sms: true } [ 'new-user' ]

Practice

Exercise: Write Nullable<T> that makes every property of T also accept null, and Setters<T> that maps name to setName(value).

Show answer
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Setters<T> = { [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void };

type User = { name: string; age: number };
const u: Nullable<User> = { name: null, age: 30 };
const s: Setters<User> = { setName: (v) => console.log(v), setAge: (v) => console.log(v) };