Frontend / Typescript / 04_utility_types_from_scratch.md

Built-in Utility Types — Reimplemented from Scratch

Updated 5 min read source
On this page6
  1. TL;DR
  2. In depth
  3. Gotchas / edge cases
  4. What a senior is expected to say
  5. Cross-references
  6. Further reading

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

In depth

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 / 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