utility-types.md 13 KB

TypeScript Utility Types Reference

Table of Contents

  1. Built-in Utility Types
  2. Custom Utility Types
  3. Type-Safe Object Operations
  4. Array/Tuple Utilities
  5. Function Utilities

Built-in Utility Types

Object Shape Utilities

// Partial<T> - Make all properties optional
interface User { id: string; name: string; email: string; }
type UpdateUser = Partial<User>; // { id?: string; name?: string; email?: string }

function updateUser(id: string, patch: Partial<User>): User { /* ... */ }

// Required<T> - Make all properties required
interface Config { host?: string; port?: number; debug?: boolean; }
type StrictConfig = Required<Config>; // { host: string; port: number; debug: boolean }

// Readonly<T> - Make all properties readonly
type ImmutableUser = Readonly<User>; // { readonly id: string; readonly name: string; ... }
const frozen: ImmutableUser = { id: '1', name: 'Alice', email: 'a@b.com' };
// frozen.name = 'Bob'; // Error: cannot assign to 'name' because it is a read-only property

// Record<K, T> - Create an object type with keys K and values T
type UserMap    = Record<string, User>;
type StatusMap  = Record<'active' | 'inactive' | 'banned', number>;
type HttpStatus = Record<200 | 404 | 500, string>;

// Pick<T, K> - Select a subset of properties
type UserPreview = Pick<User, 'id' | 'name'>; // { id: string; name: string }
type Credentials = Pick<User, 'email'>;         // { email: string }

// Omit<T, K> - Exclude specific properties
type PublicUser = Omit<User, 'email'>;   // { id: string; name: string }
type NewUser    = Omit<User, 'id'>;      // { name: string; email: string }

Union Manipulation Utilities

// Exclude<T, U> - Remove U from union T
type NonBoolean  = Exclude<string | number | boolean, boolean>; // string | number
type NonNullish  = Exclude<string | null | undefined, null | undefined>; // string
type NonString   = Exclude<string | number | boolean, string>; // number | boolean

// Extract<T, U> - Keep only members of T that are assignable to U
type OnlyStrings = Extract<string | number | boolean, string>; // string
type Primitives  = Extract<string | number | { id: string }, string | number>; // string | number

// NonNullable<T> - Remove null and undefined from T
type SafeString = NonNullable<string | null | undefined>; // string
type SafeUser   = NonNullable<User | null | undefined>;   // User

Function Utilities

function fetchData(url: string, timeout: number, headers: Record<string, string>): Promise<unknown> {
  return fetch(url);
}

// ReturnType<T> - Get the return type of a function type
type FetchResult = ReturnType<typeof fetchData>;     // Promise<unknown>
type StringLength = ReturnType<typeof String.prototype.indexOf>; // number

// Parameters<T> - Get parameters as a tuple type
type FetchParams = Parameters<typeof fetchData>;
// [url: string, timeout: number, headers: Record<string, string>]

// Call a function with stored parameters
function withDefaults<T extends (...args: unknown[]) => unknown>(
  fn: T,
  defaults: Partial<Parameters<T>>
) { /* ... */ }

// ConstructorParameters<T> - Get constructor parameter types
class HttpClient {
  constructor(baseUrl: string, timeout: number) {}
}
type HttpArgs = ConstructorParameters<typeof HttpClient>; // [string, number]

// InstanceType<T> - Get the type of a class instance
type ClientInstance = InstanceType<typeof HttpClient>; // HttpClient

// ThisParameterType<T> - Extract the type of 'this'
function greet(this: { name: string }, greeting: string): string {
  return `${greeting}, ${this.name}`;
}
type GreetThis = ThisParameterType<typeof greet>; // { name: string }

// OmitThisParameter<T> - Remove this parameter from function type
type GreetFn = OmitThisParameter<typeof greet>; // (greeting: string) => string

Awaited and String Utilities

// Awaited<T> - Recursively unwrap Promise
type A = Awaited<Promise<string>>;          // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string | Promise<number>>; // string | number

async function loadData(): Promise<User[]> { return []; }
type LoadResult = Awaited<ReturnType<typeof loadData>>; // User[]

// String manipulation (compile-time only, no runtime effect)
type UpperName = Uppercase<'hello'>;      // 'HELLO'
type LowerName = Lowercase<'WORLD'>;      // 'world'
type CapName   = Capitalize<'alice'>;     // 'Alice'
type UnCapName = Uncapitalize<'Hello'>;   // 'hello'

// Useful for generating method names
type Methods<T extends string> = `get${Capitalize<T>}` | `set${Capitalize<T>}`;
type NameMethods = Methods<'name' | 'age'>; // 'getName' | 'getAge' | 'setName' | 'setAge'

Custom Utility Types

DeepReadonly

type DeepReadonly<T> =
  T extends (infer U)[]
    ? ReadonlyArray<DeepReadonly<U>>
    : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

interface AppState {
  user: { id: string; profile: { name: string; bio: string } };
  settings: { theme: string; notifications: boolean[] };
}

type FrozenState = DeepReadonly<AppState>;
declare const state: FrozenState;
// state.user.profile.name = 'x'; // Error - deeply readonly

DeepPartial

type DeepPartial<T> =
  T extends (infer U)[]
    ? DeepPartial<U>[]
    : T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T;

// Useful for deep merge / patch operations
function deepMerge<T>(target: T, patch: DeepPartial<T>): T {
  if (typeof patch !== 'object' || patch === null) return patch as T;
  const result = { ...target };
  for (const key of Object.keys(patch) as (keyof T)[]) {
    const val = patch[key as keyof typeof patch];
    if (val !== undefined) {
      (result[key] as unknown) = typeof val === 'object' && val !== null
        ? deepMerge(result[key] as object, val as DeepPartial<object>)
        : val;
    }
  }
  return result;
}

Nullable and Optional

type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type NullableOptional<T> = T | null | undefined;

// Require at least one of specified keys
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
  Omit<T, Keys> &
  { [K in Keys]-?: Required<Pick<T, K>> & Partial<Omit<T, K>> }[Keys];

// Require exactly one of specified keys
type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
  Omit<T, Keys> &
  { [K in Keys]: Required<Pick<T, K>> & { [O in Exclude<Keys, K>]?: never } }[Keys];

Merge and UnionToIntersection

// Merge two types, second overrides first
type Merge<T, U> = Omit<T, keyof U> & U;

type A = { id: string; name: string; active: boolean };
type B = { name: number; extra: string }; // name changes type
type C = Merge<A, B>; // { id: string; active: boolean; name: number; extra: string }

// Convert a union to an intersection
type UnionToIntersection<U> =
  (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void
    ? I
    : never;

type IntersectedABC = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>;
// { a: string } & { b: number } & { c: boolean }

// Prettify - flatten intersection types for readable IDE output
type Prettify<T> = { [K in keyof T]: T[K] } & {};

Exact and StrictOmit

// Ensure no extra properties (useful in function params)
type Exact<T, Shape> = T extends Shape
  ? Exclude<keyof T, keyof Shape> extends never
    ? T
    : never
  : never;

// StrictOmit: errors if K is not in T (unlike Omit which silently ignores)
type StrictOmit<T, K extends keyof T> = Omit<T, K>;

Type-Safe Object Operations

Type-Safe pick

function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  return keys.reduce((acc, key) => {
    acc[key] = obj[key];
    return acc;
  }, {} as Pick<T, K>);
}

const user: User = { id: '1', name: 'Alice', email: 'a@b.com' };
const preview = pick(user, ['id', 'name']); // { id: string; name: string }
// TypeScript knows preview has only 'id' and 'name'

Type-Safe omit

function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
  const result = { ...obj };
  keys.forEach((key) => delete result[key]);
  return result as Omit<T, K>;
}

const publicUser = omit(user, ['email']); // { id: string; name: string }

Type-Safe merge

function merge<T extends object, U extends object>(base: T, override: U): Merge<T, U> {
  return { ...base, ...override } as Merge<T, U>;
}

type Merge<T, U> = Omit<T, keyof U> & U;

Type-Safe diff (keys present in T but not U)

type Diff<T, U> = Pick<T, Exclude<keyof T, keyof U>>;

type OnlyInA = Diff<{ a: string; b: number; c: boolean }, { b: number; d: string }>;
// { a: string; c: boolean }

Array/Tuple Utilities

Head, Tail, Last, Reverse

// First element of a tuple
type Head<T extends unknown[]> =
  T extends [infer H, ...unknown[]] ? H : never;

// All but first element
type Tail<T extends unknown[]> =
  T extends [unknown, ...infer R] ? R : never;

// Last element of a tuple
type Last<T extends unknown[]> =
  T extends [...unknown[], infer L] ? L : never;

// Reverse a tuple
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
  T extends [infer Head, ...infer Rest]
    ? Reverse<Rest, [Head, ...Acc]>
    : Acc;

type H = Head<[string, number, boolean]>; // string
type T = Tail<[string, number, boolean]>; // [number, boolean]
type L = Last<[string, number, boolean]>; // boolean
type R = Reverse<[1, 2, 3]>;             // [3, 2, 1]

Flatten Types

// Flatten one level
type Flatten<T extends unknown[]> =
  T extends (infer U)[] ? U : T;

type F = Flatten<string[][]>; // string[]

// Flatten nested arrays recursively
type DeepFlatten<T> =
  T extends (infer U)[]
    ? U extends unknown[]
      ? DeepFlatten<U>
      : U
    : T;

type Deep = DeepFlatten<string[][][]>; // string

Zip Two Tuples

type Zip<T extends unknown[], U extends unknown[]> =
  T extends [infer TH, ...infer TR]
    ? U extends [infer UH, ...infer UR]
      ? [[TH, UH], ...Zip<TR, UR>]
      : []
    : [];

type Zipped = Zip<[1, 2, 3], ['a', 'b', 'c']>;
// [[1, 'a'], [2, 'b'], [3, 'c']]

Length and Indices

type Length<T extends readonly unknown[]> = T['length'];

// Generate numeric union of indices
type Indices<T extends readonly unknown[]> =
  Exclude<keyof T, keyof []>;

type Len = Length<[1, 2, 3]>; // 3
type Idx = Indices<['a', 'b', 'c']>; // '0' | '1' | '2'

Function Utilities

Promisify Type

// Convert a callback-style function type to one returning a Promise
type Promisify<T extends (...args: unknown[]) => unknown> =
  T extends (...args: infer A) => infer R
    ? R extends Promise<unknown>
      ? T
      : (...args: A) => Promise<Awaited<R>>
    : never;

type SyncFn = (x: number) => string;
type AsyncFn = Promisify<SyncFn>; // (x: number) => Promise<string>

Curry Type

type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;

type Curried<TArgs extends unknown[], TReturn> =
  TArgs extends []
    ? TReturn
    : (arg: Head<TArgs>) => Curried<Tail<TArgs>, TReturn>;

declare function curry<TArgs extends unknown[], TReturn>(
  fn: (...args: TArgs) => TReturn
): Curried<TArgs, TReturn>;

const add = curry((a: number, b: number) => a + b);
const inc = add(1);     // Curried<[number], number> = (b: number) => number
const two = inc(1);     // number

Overload Helper

// Extract all overload signatures as a union
type Overloads<T extends (...args: unknown[]) => unknown> =
  T extends {
    (...args: infer A1): infer R1;
    (...args: infer A2): infer R2;
    (...args: infer A3): infer R3;
    (...args: infer A4): infer R4;
  }
    ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3) | ((...args: A4) => R4)
    : T extends {
        (...args: infer A1): infer R1;
        (...args: infer A2): infer R2;
        (...args: infer A3): infer R3;
      }
    ? ((...args: A1) => R1) | ((...args: A2) => R2) | ((...args: A3) => R3)
    : T extends { (...args: infer A1): infer R1; (...args: infer A2): infer R2 }
    ? ((...args: A1) => R1) | ((...args: A2) => R2)
    : T;

Memoize with Type Safety

type AnyFn = (...args: unknown[]) => unknown;

function memoize<T extends AnyFn>(fn: T): T {
  const cache = new Map<string, ReturnType<T>>();
  return ((...args: Parameters<T>): ReturnType<T> => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key) as ReturnType<T>;
    const result = fn(...args) as ReturnType<T>;
    cache.set(key, result);
    return result;
  }) as T;
}

const expensiveCalc = memoize((a: number, b: number): number => a * b);
expensiveCalc(2, 3); // 6 - computed
expensiveCalc(2, 3); // 6 - cached