Skip to content
Snippets

3 min read

TypeScript utilities

Small type helpers I reach for in every project: Prettify, DeepPartial, assertNever, typed entries and infer.

  • ts

A handful of small types and functions that make TypeScript friendlier. None of them needs a library, and each is a few lines you can paste into a types.ts.

Prettify

Intersections like A & B & C show up in editor hovers as exactly that, which is hard to read. Prettify flattens them into one plain object type, so you see the final shape.

ts
export type Prettify<T> = { [K in keyof T]: T[K] } & {};
ts
type WithId = { id: string } & { name: string };
type Flat = Prettify<WithId>; // { id: string; name: string }

DeepPartial

Partial<T> only makes the top level optional. DeepPartial goes all the way down, which is handy for patches and test fixtures. Functions are left alone, and arrays keep their element type.

ts
export type DeepPartial<T> = T extends (...args: never[]) => unknown
  ? T
  : T extends readonly (infer U)[]
    ? DeepPartial<U>[]
    : T extends object
      ? { [K in keyof T]?: DeepPartial<T[K]> }
      : T;
ts
type Account = { id: string; profile: { name: string; social: { x?: string } } };

const patch: DeepPartial<Account> = { profile: { social: { x: 'jana__sundar' } } };

assertNever

Put it in the default branch of a switch and TypeScript checks that you handled every case. Add a new value to the union later, and every switch that forgot it stops compiling.

ts
export function assertNever(value: never, message = `Unexpected value: ${String(value)}`): never {
  throw new Error(message);
}
ts
type Status = 'idle' | 'loading' | 'error';

function label(status: Status) {
  switch (status) {
    case 'idle':
      return 'Ready';
    case 'loading':
      return 'Loading…';
    case 'error':
      return 'Something went wrong';
    default:
      return assertNever(status); // an error here means a case is missing
  }
}

Typed keys and entries

Object.keys and Object.entries return plain string keys, throwing away the types you already have. These keep them:

ts
export const typedKeys = <T extends object>(obj: T) => Object.keys(obj) as (keyof T)[];

export const typedEntries = <T extends object>(obj: T) =>
  Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][];
ts
const prices = { daily: 240, monthly: 4800 };

for (const [plan, price] of typedEntries(prices)) {
  // plan: 'daily' | 'monthly', price: number
}

A small lie

These casts assume the object has exactly the keys its type says. An object can have extra keys at runtime, so use them on data you control, not on anything that comes from outside.

Infer instead of repeating yourself

The infer keyword lets a type pull a piece out of another type, so you don't write the same shape twice.

ts
export type ElementOf<T> = T extends readonly (infer U)[] ? U : never;

export type ResolvedReturn<T extends (...args: never[]) => unknown> = Awaited<ReturnType<T>>;
ts
const roles = ['admin', 'editor', 'viewer'] as const;
type Role = ElementOf<typeof roles>; // 'admin' | 'editor' | 'viewer'

async function getUser() {
  return { id: '1', name: 'Jana' };
}
type Loaded = ResolvedReturn<typeof getUser>; // { id: string; name: string }

Add a role to the array and Role updates by itself. Change what getUser returns and Loaded follows.