frontend / typescript / 04_utility_types_from_scratch.md

Built-in Utility Types — Reimplemented from Scratch

5 min read source

Built-in Utility Types — Reimplemented from Scratch

TL;DR

Every “magic” utility type in TypeScript (Partial, Pick, Record, ReturnType, Awaited, …) is a thin wrapper around the primitives in 01_generics.md, 02_conditional_and_mapped_types.md, and 03_template_literal_types.md. A senior interview will sometimes ask you to reimplement one on the spot — the test is whether you understand the primitives.

Interview Q&A

Q: Reimplement Partial<T>, Required<T>, Readonly<T>.

All three are mapped types with a single modifier:

type MyPartial<T>  = { [K in keyof T]?: T[K] }
type MyRequired<T> = { [K in keyof T]-?: T[K] }   // -? strips ?
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }
type MyMutable<T>  = { -readonly [K in keyof T]: T[K] }   // not built-in, but trivial

Q: Reimplement Pick<T, K> and Omit<T, K>.

type MyPick<T, K extends keyof T> = { [P in K]: T[P] }

type MyOmit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

Omit uses key remapping with as never to filter out matching keys. (TS’s built-in Omit is equivalent to Pick<T, Exclude<keyof T, K>>.)

Q: Reimplement Record<K, V>.

type MyRecord<K extends keyof any, V> = { [P in K]: V }

keyof any = string | number | symbol — the set of valid index types.

Q: Reimplement Exclude<T, U> and Extract<T, U>.

Both rely on distributive conditional types — they apply to each member of a union independently.

type MyExclude<T, U> = T extends U ? never : T
type MyExtract<T, U> = T extends U ? T : never

type A = MyExclude<'a' | 'b' | 'c', 'a'>   // "b" | "c"
type B = MyExtract<'a' | 'b' | 'c', 'a'>   // "a"

Q: Reimplement NonNullable<T>.

type MyNonNullable<T> = T extends null | undefined ? never : T
// Or with Exclude:
type MyNonNullable2<T> = Exclude<T, null | undefined>

Strips null | undefined from a union.

Q: Reimplement ReturnType<F>.

infer in the return position:

type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never

type A = MyReturnType<() => number>           // number
type B = MyReturnType<(x: string) => boolean> // boolean
type C = MyReturnType<'not a function'>       // never

Q: Reimplement Parameters<F>.

type MyParameters<F> = F extends (...args: infer P) => any ? P : never

type A = MyParameters<(x: string, y: number) => void>   // [x: string, y: number]

The result is a tuple — preserves names if the original function has labelled tuple parameters.

Q: Reimplement ConstructorParameters<C> and InstanceType<C>.

type MyConstructorParameters<C> =
  C extends new (...args: infer P) => any ? P : never

type MyInstanceType<C> =
  C extends new (...args: any[]) => infer R ? R : never

class User { constructor(public name: string, public age: number) {} }

type P = MyConstructorParameters<typeof User>   // [name: string, age: number]
type I = MyInstanceType<typeof User>            // User

new (...) => R is the constructor-signature form (vs (...) => R for plain functions).

Q: Reimplement Awaited<T>.

Recursively unwraps nested promises:

type MyAwaited<T> =
  T extends Promise<infer V>
    ? MyAwaited<V>
    : T

type A = MyAwaited<Promise<string>>                       // string
type B = MyAwaited<Promise<Promise<number>>>              // number
type C = MyAwaited<string>                                // string

The built-in Awaited also handles thenables more carefully.

Q: Reimplement Uppercase, Lowercase, Capitalize, Uncapitalize?

You can’t reimplement these in pure TS — they’re intrinsic types, implemented inside the compiler. You can use them, but not define them yourself.

Q: What does ThisParameterType<F> / OmitThisParameter<F> do?

Extract or strip the this parameter from a function type.

type T = ThisParameterType<(this: User, x: number) => void>   // User
type F = OmitThisParameter<(this: User, x: number) => void>   // (x: number) => void

Useful for typing Function.prototype.bind and related patterns.

Q: Why is reimplementing these worth knowing?

Three reasons:

  1. Read library types. Big libraries (TanStack, tRPC, zod) define their own conditional-mapped types. Recognising the pattern means you can read them.
  2. Build the missing one. TS doesn’t ship Mutable<T>, DeepPartial<T>, PickByValue<T, V>, OmitByValue<T, V>, Keys<T, V> (filter to keys whose value extends V) — you build those yourself.
  3. Interview signal. Being able to write type MyPick<T, K extends keyof T> = { [P in K]: T[P] } from memory marks you as someone who understands the type system instead of someone who knows the names of utilities.

Q: Build DeepPartial<T> and DeepReadonly<T>.

type DeepPartial<T> =
  T extends (...a: any[]) => any ? T :
  T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } :
  T

type DeepReadonly<T> =
  T extends (...a: any[]) => any ? T :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T

Skip functions (don’t make them partial/readonly), recurse on objects, leave primitives alone.

Q: Build PickByValue<T, V> (filter keys whose value type extends V).

type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K]
}

interface User { id: number; name: string; active: boolean }
type Strings = PickByValue<User, string>   // { name: string }

Same as never filter trick as Omit.

Gotchas / edge cases

  • Pick<T, 'noSuchKey'> errors at compile time because of K extends keyof T. Good.
  • Omit<T, 'noSuchKey'> silently does nothing because K extends keyof any — wider. There’s a common stricter version: type StrictOmit<T, K extends keyof T> = Omit<T, K>.
  • Partial<T> is shallow. Use DeepPartial<T> for nested.
  • Awaited<T> is recursiveAwaited<Promise<Promise<X>>> is X, not Promise<X>.
  • Parameters<F> returns a labeled tuple type in TS 4.0+, preserving param names.
  • Iterating keyof T skips inherited / non-enumerable keys — same as for...in semantics at the type level.

What a senior is expected to say

A junior says “I’d use Partial<User>.” A senior says “Partial is just { [K in keyof T]?: T[K] } — and here’s DeepPartial if you need it, here’s PickByValue to filter keys by value type, here’s how I’d build a RequireOnly<T, K> that makes some props required and the rest optional.” The signal is fluency at the primitive level: mapped + conditional + infer + key remapping — not memorising names of utilities.

Cross-references

Further reading