Phase 6 · Utility types in practice · Lesson 6.2
AdvancedBuild the utility types yourself
Re-implement Partial, Pick, Omit, Exclude, ReturnType, Awaited and friends from scratch, then go further with DeepReadonly, DeepPartial and Mutable.
30 min
"Implement Pick without using Pick" is one of the most common TypeScript interview questions. It's a good question: you can't answer it by memory, only by understanding the three tools every utility type is built from. Once you've built them yourself, the built-ins stop being magic and you can write your own when none of them fits.
The toolbox
Every utility type in this lesson uses some mix of these:
| Tool | Syntax | What it does |
|---|---|---|
keyof | keyof T | The union of T's keys. |
| Indexed access | T[K] | The type of property K in T. |
| Mapped type | { [K in Keys]: ... } | Build an object type by looping over a union of keys. |
| Modifiers | ?, readonly, -?, -readonly | Add or remove optional / readonly in a mapped type. |
| Key remapping | { [K in keyof T as NewKey]: ... } | Rename keys, or drop them by mapping to never. |
| Conditional type | T extends U ? X : Y | Branch on assignability. Distributes over unions when T is a bare type parameter. |
infer | T extends Promise<infer V> ? V : never | Capture part of a type inside a conditional. |
Each exercise below comes with a statement, a hint, and a solution in a Reveal. Try it first (the playground link on the first block is a good scratchpad). Every solution is verified with the same two helpers:
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type _pass = Expect<Equal<string, string>>;
// @ts-expect-error -- Type 'false' does not satisfy the constraint 'true'.
type _fail = Expect<Equal<string, "a">>;Equal is stricter than "assignable both ways": it treats readonly, ? and any as differences. That's what you want when checking a type-level implementation.
Exercise 1: MyPartial
Make every property of T optional.
Hint: loop over keyof T with a mapped type, and add the ? modifier.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyPartial<T> = { [K in keyof T]?: T[K] };
type User = { id: number; readonly name: string };
type _t1 = Expect<Equal<MyPartial<User>, { id?: number; readonly name?: string }>>;
type _t2 = Expect<Equal<MyPartial<User>, Partial<User>>>;▶ Try it in the TypeScript Playground
[K in keyof T] visits every key; T[K] keeps each value type; ? makes it optional. Notice readonly survived. A mapped type written as [K in keyof T] is homomorphic: it copies the original modifiers of each property unless you change them. That's why the built-ins can be one-liners.
Exercise 2: MyRequired
Make every property required.
Hint: modifiers can be removed as well as added.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type Settings = { theme?: string; fontSize?: number | undefined };
type _t1 = Expect<Equal<MyRequired<Settings>, { theme: string; fontSize: number }>>;
type _t2 = Expect<Equal<MyRequired<Settings>, Required<Settings>>>;-? removes the optional modifier. It also strips undefined from the value type, which is why fontSize is number, not number | undefined. (With exactOptionalPropertyTypes enabled, an explicitly written | undefined is kept.)
Exercise 3: MyReadonly and Mutable
Write MyReadonly<T> (every property readonly), then its opposite Mutable<T> (remove readonly everywhere). Mutable is not built in, but you'll want it.
Hint: same pattern as exercise 2, with readonly and -readonly.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Point = { x: number; y: number };
type _t1 = Expect<Equal<MyReadonly<Point>, Readonly<Point>>>;
type _t2 = Expect<Equal<Mutable<Readonly<Point>>, Point>>;
// Homomorphic mapped types also understand arrays and tuples:
type _t3 = Expect<Equal<MyReadonly<string[]>, readonly string[]>>;
type _t4 = Expect<Equal<Mutable<readonly [1, 2]>, [1, 2]>>;Mutable is handy for builders and tests that need to tweak an object typed as readonly, e.g. one that came from as const.
Quick check
Which of these is NOT a valid mapped type modifier?
Exercise 4: MyPick
MyPick<T, K> keeps only the keys in K. It must reject keys that aren't in T.
Hint: constrain K, then loop over K instead of keyof T.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type User = { id: number; name?: string; email: string };
type _t1 = Expect<Equal<MyPick<User, "id" | "name">, { id: number; name?: string }>>;
type _t2 = Expect<Equal<MyPick<User, "id">, Pick<User, "id">>>;
// @ts-expect-error -- Type '"age"' does not satisfy the constraint 'keyof User'.
type _t3 = MyPick<User, "age">;Two details interviewers look for:
K extends keyof Tis what makes typos a compile error, and it's also what makesT[P]legal (the compiler knowsPis a key ofT).namekept its?. Mapping over a type parameter constrained tokeyof Tstill counts as homomorphic, so modifiers are copied.
Exercise 5: MyExclude and MyExtract
MyExclude<T, U> removes from union T every member assignable to U. MyExtract<T, U> keeps only those members.
Hint: a conditional type on a bare type parameter runs once per union member. Returning never for a member removes it, because X | never is X.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type Status = "active" | "banned" | "pending";
type _t1 = Expect<Equal<MyExclude<Status, "banned">, "active" | "pending">>;
type _t2 = Expect<Equal<MyExtract<string | number | boolean, string | boolean>, string | boolean>>;Step by step, MyExclude<"active" | "banned" | "pending", "banned"> becomes:
("active" extends "banned" ? never : "active") | ("banned" extends "banned" ? never : "banned") | ("pending" extends ...)"active" | never | "pending""active" | "pending"
That per-member behaviour is called distribution. It only happens when the checked type is a naked type parameter. Wrap it in a tuple, [T] extends [U], and the union is checked as a whole.
Quick check
What is R?
type NotNull<T> = [T] extends [null | undefined] ? never : T;
type R = NotNull<string | null>;Exercise 6: MyOmit
MyOmit<T, K> removes keys K from T. Like the built-in, accept any key type.
Hint: there are two clean solutions. One reuses exercises 4 and 5. The other uses key remapping with as.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyExclude<T, U> = T extends U ? never : T;
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Version 1: pick the keys that are left
type MyOmit<T, K extends PropertyKey> = MyPick<T, MyExclude<keyof T, K>>;
// Version 2: key remapping, mapping unwanted keys to never drops them
type MyOmit2<T, K extends PropertyKey> = { [P in keyof T as P extends K ? never : P]: T[P] };
type User = { readonly id: number; name?: string; password: string };
type _t1 = Expect<Equal<MyOmit<User, "password">, { readonly id: number; name?: string }>>;
type _t2 = Expect<Equal<MyOmit2<User, "password">, { readonly id: number; name?: string }>>;
type _t3 = Expect<Equal<MyOmit<User, "password">, Omit<User, "password">>>;Version 1 is exactly how lib.es5.d.ts defines Omit. Version 2 is often preferred in modern code because the as clause can do more (filter by value type, rename keys). Both keep readonly and ?.
PropertyKey is a built-in alias for string | number | symbol, the same as keyof any. Using it instead of keyof T is why Omit doesn't catch typos (see the previous lesson). Write K extends keyof T for a strict version.
Exercise 7: MyRecord
MyRecord<K, V> is an object with keys K, each with value V.
Hint: this mapped type does not loop over keyof anything.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyRecord<K extends PropertyKey, V> = { [P in K]: V };
type _t1 = Expect<Equal<MyRecord<"a" | "b", number>, { a: number; b: number }>>;
type _t2 = Expect<Equal<MyRecord<string, boolean>, { [key: string]: boolean }>>;
type _t3 = Expect<Equal<MyRecord<"x", 1>, Record<"x", 1>>>;K extends PropertyKey is needed because only strings, numbers and symbols can be keys. Mapping over string produces an index signature, not a finite set of properties. Since it doesn't map over keyof T, this mapped type is not homomorphic: there are no original modifiers to copy.
Exercise 8: MyNonNullable
Remove null and undefined from T.
Hint: either a distributive conditional, or an intersection with the "any non-nullish value" type.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyNonNullable<T> = T & {};
type MyNonNullable2<T> = T extends null | undefined ? never : T;
type _t1 = Expect<Equal<MyNonNullable<string | null | undefined>, string>>;
type _t2 = Expect<Equal<MyNonNullable2<number | undefined>, number>>;
type _t3 = Expect<Equal<MyNonNullable<string | null>, NonNullable<string | null>>>;{} means "any value except null and undefined". Intersecting distributes over the union: (string & {}) | (null & {}) is string | never. TypeScript 4.8 switched the built-in to T & {} because it behaves better with unresolved generics: an intersection can be simplified and narrowed while T is still unknown, whereas a conditional type on a generic T stays deferred until T is known.
Exercise 9: MyReturnType and MyParameters
MyReturnType<F> is what function type F returns; MyParameters<F> is its parameter list as a tuple.
Hint: this is what infer is for. Match F against a function type and capture the part you want.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyReturnType<F extends (...args: any) => any> = F extends (...args: any) => infer R ? R : never;
type MyParameters<F extends (...args: any) => any> = F extends (...args: infer P) => any ? P : never;
type Fn = (id: number, verbose?: boolean) => Promise<string>;
type _t1 = Expect<Equal<MyReturnType<Fn>, Promise<string>>>;
type _t2 = Expect<Equal<MyParameters<Fn>, [id: number, verbose?: boolean]>>;
type _t3 = Expect<Equal<MyParameters<() => void>, []>>;
// @ts-expect-error -- Type 'string' does not satisfy the constraint '(...args: any) => any'.
type _t4 = MyReturnType<string>;Reasoning:
- The constraint
F extends (...args: any) => anyrejects non-functions up front. Without it,MyReturnType<string>would quietly benever. - The conditional is still needed, because
inferonly works inside theextendsclause of a conditional type. (...args: any)rather than(...args: unknown[]): because parameters are checked contravariantly, a function taking(id: number)is not assignable to(...args: unknown[]) => any.anyaccepts every parameter list.
Exercise 10: MyAwaited
Unwrap a promise type, recursively: MyAwaited<Promise<Promise<number>>> is number. Non-promises pass through.
Hint: infer again, plus recursion. Match against PromiseLike rather than Promise so any "thenable" works.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T;
type _t1 = Expect<Equal<MyAwaited<Promise<string>>, string>>;
type _t2 = Expect<Equal<MyAwaited<Promise<Promise<number>>>, number>>;
type _t3 = Expect<Equal<MyAwaited<boolean | Promise<string>>, boolean | string>>;
type _t4 = Expect<Equal<MyAwaited<PromiseLike<Date>>, Date>>;It distributes over unions (bare T) and calls itself until the type is no longer thenable. Recursive conditional types are fine as long as they terminate; the compiler caps instantiation depth, so infinitely recursive types become an error rather than a hang.
The real Awaited is a bit longer: it matches any object with a then(onfulfilled, ...) method (not just PromiseLike), returns never for a thenable whose then doesn't take a callback, and special-cases null/undefined. The idea is the same.
Going deeper: DeepReadonly
The built-in Readonly is shallow. Write DeepReadonly<T> that makes every nested property readonly, including arrays, but leaves functions alone.
Hint: recurse into T[K]. Stop at primitives and functions. Homomorphic mapped types already turn arrays into readonly arrays.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Config = {
server: { host: string; ports: number[] };
pair: [string, number];
onReady: () => void;
};
type _t1 = Expect<Equal<DeepReadonly<Config>, {
readonly server: { readonly host: string; readonly ports: readonly number[] };
readonly pair: readonly [string, number];
readonly onReady: () => void;
}>>;
declare const cfg: DeepReadonly<Config>;
// @ts-expect-error -- Property 'push' does not exist on type 'readonly number[]'.
cfg.server.ports.push(80);Why each branch:
- Functions first. Functions are objects, so without this branch they'd be mapped into
{}-like types and lose their call signature. T extends objectcovers plain objects, arrays and tuples. Mapping a homomorphic type over an array type yields a (readonly) array, not an object withlengthandpushkeys.- Everything else (primitives) is returned as is.
Edge case worth mentioning in an interview: class instances like Date or Map are objects too, so they get mapped into plain readonly objects and stop being assignable to Date/Map. Production versions add branches for them (e.g. T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : ...).
Going deeper: DeepPartial
DeepPartial<T> makes every nested property optional. Useful for config overrides and test fixtures.
Hint: same shape as DeepReadonly, with ? instead of readonly.
Show the solution
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = { server: { host: string; port: number }; tags: string[] };
type _t1 = Expect<Equal<DeepPartial<Config>, {
server?: { host?: string; port?: number };
tags?: (string | undefined)[];
}>>;
const override: DeepPartial<Config> = { server: { port: 8080 } }; // host can be left outLook closely at tags: applying ? to an array makes its elements string | undefined. That's usually not what you want. A common refinement keeps arrays as they are:
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends readonly unknown[]
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;Spot the error
A candidate wrote this MyPick. It doesn't compile. Why, and what's the fix?
type MyPick<T, K> = { [P in K]: T[P] };Show the answer
Two errors, both caused by the missing constraint on K:
[P in K]: Type 'K' is not assignable to type 'string | number | symbol'. A mapped type can only loop over valid key types.T[P]: Type 'P' cannot be used to index type 'T'. Nothing saysPis a key ofT.
Constrain K to the keys of T and both go away:
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type _t1 = Expect<Equal<MyPick<{ a: 1; b: 2 }, "a">, { a: 1 }>>;Recap
- Mapped types (
[K in keyof T]) build objects key by key;T[K]reads a property type. - Modifiers
?/readonlycan be added or removed (-?,-readonly); homomorphic mapped types copy the original ones. Pickloops over a constrainedK extends keyof T;Recordloops over anyPropertyKeyunion.Exclude,Extract,NonNullablerely on distributive conditional types;[T] extends [U]turns distribution off.Omit=Pick+Exclude, or key remapping withas ... never.infercaptures parts of a type: return types, parameter tuples, promise values. Recursion handles nesting.- Deep variants recurse into
T[K], stop at primitives and functions, and need care with arrays and built-in classes.
Interview cards
1 / 8