Frontend / Typescript / 04_utility_types_from_scratch.md

Built-in Utility Types — Reimplemented from Scratch

Updated 5 min read source
On this page18 Reimplement Partial, Required, Readonly.
  • Reimplement Pick and Omit.
  • Reimplement Record.
  • Reimplement Exclude and Extract.
  • Reimplement NonNullable.Reimplement ReturnType.Reimplement Parameters.Reimplement ConstructorParameters and InstanceType.Reimplement Awaited.
  • Reimplement Uppercase, Lowercase, Capitalize, Uncapitalize?
  • What does ThisParameterType / OmitThisParameter do?
  • Why is reimplementing these worth knowing?
  • Build DeepPartial and DeepReadonly.
  • Build PickByValue (filter keys whose value type extends V).
  • Gotchas
  • Related
  • Further reading
  • Interview angle
  • Built-in Utility Types — Reimplemented from Scratch

    Every “magic” utility type in TypeScript (Partial, Pick, Record, ReturnType, Awaited, …) is a thin wrapper around the primitives in TypeScript Generics, Conditional and Mapped Types (and infer), and Template Literal Types. A senior interview will sometimes ask you to reimplement one on the spot — the test is whether you understand the primitives.

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

    All three are mapped types with a single modifier:

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

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

    ts
    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>>.)

    Reimplement Record<K, V>.

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

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

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

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

    ts
    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"

    Reimplement NonNullable<T>.

    ts
    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.

    Reimplement ReturnType<F>.

    infer in the return position:

    ts
    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

    Reimplement Parameters<F>.

    ts
    type MyParameters<F> = F extends (...args: infer P) => any ? P : never
    
    // [x: string, y: number]
    type A = MyParameters<(x: string, y: number) => void>

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

    Reimplement ConstructorParameters<C> and InstanceType<C>.

    ts
    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).

    Reimplement Awaited<T>.

    Recursively unwraps nested promises:

    ts
    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.

    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.

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

    Extract or strip the this parameter from a function type.

    ts
    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.

    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.

    Build DeepPartial<T> and DeepReadonly<T>.

    ts
    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.

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

    ts
    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 }
    // { name: string }
    type Strings = PickByValue<User, string>

    Same as never filter trick as Omit.

    Gotchas

    • 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 recursive — Awaited<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.

    Further reading

    Interview angle

    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.