Skip to content

Utility Types from Scratch

intermediate12 min read

Stop Memorizing, Start Understanding

TypeScript ships with utility types like Partial<T>, Pick<T, K>, and Omit<T, K>. Most developers memorize what they do without understanding how they work. But every utility type is built from the same three primitives you already know: mapped types, conditional types, and keyof. Build them once from scratch and you'll never be confused by them again.

Mental Model

Think of utility types as cookie cutters. Each one takes a type (the dough) and cuts it into a specific shape. Partial pokes holes (makes everything optional). Pick cuts out specific pieces. Omit removes pieces. Record stamps out a grid pattern. Once you see the cookie cutter mechanism, you can build any shape you need.

Partial<T> — Make Everything Optional

// Built-in
type User = { name: string; age: number; email: string };
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

// From scratch — it's just a mapped type with ?
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

The ? after ] adds optionality to every property. keyof T gives the union of property names, in iterates over them, and T[K] preserves the original value type.

Required<T> — Make Everything Required

// Built-in
type RequiredUser = Required<PartialUser>;
// { name: string; age: number; email: string }

// From scratch — -? removes optionality
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

The -? modifier is the inverse of ?. It strips the optional marker from every property.

Readonly<T> — Make Everything Readonly

// Built-in
type FrozenUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

// From scratch
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

Pick<T, K> — Select Specific Properties

// Built-in
type NameAndAge = Pick<User, "name" | "age">;
// { name: string; age: number }

// From scratch — iterate over K instead of keyof T
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

The constraint K extends keyof T ensures you can only pick properties that actually exist on T. Instead of iterating over all keys (keyof T), we iterate over the subset K.

Omit<T, K> — Remove Specific Properties

// Built-in
type WithoutEmail = Omit<User, "email">;
// { name: string; age: number }

// From scratch — Pick everything EXCEPT K
type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

// Alternative using Exclude + Pick
type MyOmit2<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Two ways to build Omit and when each is better

The key remapping version (as P extends K ? never : P) is a single mapped type — it iterates once. The Pick + Exclude version composes two utilities.

The key remapping version preserves modifiers (readonly, optional) exactly. The composed version also preserves modifiers because Pick and Exclude both do. In practice, both are equivalent.

Note that TypeScript's built-in Omit uses Pick<T, Exclude<keyof T, K>> and does NOT constrain K to keyof T. This means you can Omit<User, "nonexistent"> without error. Some teams prefer the stricter version that errors on invalid keys.

Record<K, V> — Create an Object Type from Keys and Values

// Built-in
type PageViews = Record<string, number>;
// { [key: string]: number }

type StatusLabels = Record<"active" | "inactive" | "banned", string>;
// { active: string; inactive: string; banned: string }

// From scratch — map over K and set each value to V
type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

keyof any is string | number | symbol — the set of all valid property key types.

Exclude<T, U> and Extract<T, U> — Filter Unions

// Exclude: remove members of T that are assignable to U
type WithoutNull = Exclude<string | number | null, null>;
// string | number

// From scratch — conditional type that distributes
type MyExclude<T, U> = T extends U ? never : T;

// Extract: keep only members of T that are assignable to U
type OnlyStrings = Extract<string | number | boolean, string>;
// string

// From scratch
type MyExtract<T, U> = T extends U ? T : never;

These work because conditional types distribute over unions. Exclude<string | number | null, null> evaluates as:

  • string extends null ? never : stringstring
  • number extends null ? never : numbernumber
  • null extends null ? never : nullnever
  • Result: string | number | neverstring | number

ReturnType<T> and Parameters<T> — Extract Function Types

// ReturnType: get the return type of a function
type R = ReturnType<() => string>; // string
type R2 = ReturnType<typeof fetch>; // Promise<Response>

// From scratch — infer the return type
type MyReturnType<T extends (...args: any[]) => any> =
  T extends (...args: any[]) => infer R ? R : never;

// Parameters: get the parameter types as a tuple
type P = Parameters<(a: string, b: number) => void>;
// [string, number]

// From scratch
type MyParameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;
Common Trap

ReturnType and Parameters require typeof when used with named functions:

function greet(name: string): string { return `Hello, ${name}`; }

type R = ReturnType<greet>;        // ERROR: 'greet' refers to a value
type R = ReturnType<typeof greet>; // OK: string

// This is because greet is a value (a function), not a type.
// typeof converts a value to its type.

NonNullable<T> — Remove null and undefined

// Built-in
type Clean = NonNullable<string | null | undefined>;
// string

// From scratch — just Exclude with null | undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;

Production Scenario: Composing Utility Types

// API update endpoint — all fields optional except id
type UpdatePayload<T extends { id: string }> = Partial<Omit<T, "id">> & Pick<T, "id">;

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
  lastLogin: Date;
}

type UpdateUser = UpdatePayload<User>;
// {
//   id: string;          (required — from Pick)
//   name?: string;       (optional — from Partial<Omit>)
//   email?: string;
//   role?: "admin" | "user";
//   lastLogin?: Date;
// }

// Type-safe form field configuration
type FormFields<T> = {
  [K in keyof T]: {
    label: string;
    validate: (value: T[K]) => string | null;
    defaultValue: T[K];
  };
};

const userForm: FormFields<Pick<User, "name" | "email">> = {
  name: {
    label: "Full Name",
    validate: (v) => v.length < 2 ? "Too short" : null, // v: string
    defaultValue: "",
  },
  email: {
    label: "Email Address",
    validate: (v) => v.includes("@") ? null : "Invalid email", // v: string
    defaultValue: "",
  },
};
Execution Trace
Omit:
`OmitUser, 'id'`
Remove id from User → `( name, email, role, lastLogin )`
Partial:
`PartialOmitUser, 'id'`
Make remaining fields optional → `( name?, email?, role?, lastLogin? )`
Pick:
`PickUser, 'id'`
Extract id as required → `( id: string )`
Intersect:
`Partial... & Pick...`
Combine: id required, everything else optional
Common Mistakes
  • Wrong: Using Omit to remove many props when Pick would be simpler Right: If you need 2 of 10 props, use Pick. If you need 8 of 10, use Omit.

  • Wrong: Confusing Exclude (for unions) with Omit (for object properties) Right: Exclude filters union members. Omit removes object properties. Different operations.

  • Wrong: Forgetting typeof when using ReturnType/Parameters with function values Right: Use ReturnType<typeof myFunction> — typeof converts the value to its type

  • Wrong: Building complex one-off utility types instead of composing built-ins Right: Compose: Partial<Omit<T, K>> & Pick<T, K> is clearer than a custom mapped type

Quiz
What is Exclude<'a' | 'b' | 'c', 'a' | 'c'>?
Quiz
How does MyPartial<T> = { [K in keyof T]?: T[K] } actually work?
Quiz
Why does the built-in Omit<T, K> NOT constrain K extends keyof T?
Quiz
You need a type where id is required but all other User properties are optional. Which composition is correct?

Build a MakeRequired Utility

Key Rules
  1. 1Every utility type is built from mapped types, conditional types, and keyof — learn the primitives and you can build anything
  2. 2Partial adds ?, Required removes with -?, Readonly adds readonly, Mutable removes with -readonly
  3. 3Pick selects properties by key (positive selection), Omit removes by key (negative selection)
  4. 4Exclude and Extract filter union members — they operate on unions, not object properties
  5. 5Compose built-in utilities for readability: Partial<Omit<T, K>> & Pick<T, K> over custom mapped types