Skip to content
TR

Phase 6 · Utility types in practice · Lesson 6.2

Advanced

Build 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:

ToolSyntaxWhat it does
keyofkeyof TThe union of T's keys.
Indexed accessT[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, -?, -readonlyAdd 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 typeT extends U ? X : YBranch on assignability. Distributes over unions when T is a bare type parameter.
inferT extends Promise<infer V> ? V : neverCapture 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 T is what makes typos a compile error, and it's also what makes T[P] legal (the compiler knows P is a key of T).
  • name kept its ?. Mapping over a type parameter constrained to keyof T still 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:

  1. ("active" extends "banned" ? never : "active") | ("banned" extends "banned" ? never : "banned") | ("pending" extends ...)
  2. "active" | never | "pending"
  3. "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) => any rejects non-functions up front. Without it, MyReturnType<string> would quietly be never.
  • The conditional is still needed, because infer only works inside the extends clause 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. any accepts 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:

  1. Functions first. Functions are objects, so without this branch they'd be mapped into {}-like types and lose their call signature.
  2. T extends object covers plain objects, arrays and tuples. Mapping a homomorphic type over an array type yields a (readonly) array, not an object with length and push keys.
  3. 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 out

Look 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 says P is a key of T.

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 ? / readonly can be added or removed (-?, -readonly); homomorphic mapped types copy the original ones.
  • Pick loops over a constrained K extends keyof T; Record loops over any PropertyKey union.
  • Exclude, Extract, NonNullable rely on distributive conditional types; [T] extends [U] turns distribution off.
  • Omit = Pick + Exclude, or key remapping with as ... never.
  • infer captures 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