Careeroza
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) };