cheat sheet

type-fest

Sindre Sorhus's collection of essential TypeScript utility types — PartialDeep, ReadonlyDeep, SetOptional, RequireAtLeastOne, Merge, Tagged, JsonValue, Opaque, and dozens more — so you don't hand-roll them.

type-fest — Community utility types for TypeScript

What it is

type-fest is a collection of essential, hand-curated TypeScript utility types maintained by Sindre Sorhus — the same author behind chalk, got, and dozens of other npm staples. It ships only types (zero runtime code), is published as ESM, and depends on nothing. Use it when you find yourself about to hand-roll a deep variant of Partial, a "require at least one key" union, an Opaque/Tagged brand, or a recursive JsonValuetype-fest already has it, with edge cases handled. Alternatives are the much smaller ts-toolbelt, the (mostly defunct) utility-types, or just writing your own; type-fest is the de-facto community standard.

The library complements TS's built-in utility typesPartial, Required, Pick, Omit, etc. — by filling the gaps the standard library leaves open.

Install

type-fest is a dev-only dependency since it emits no runtime code. Install with any package manager.

bash
# npm
npm install -D type-fest

# pnpm
pnpm add -D type-fest

# yarn
yarn add -D type-fest

# bun
bun add -d type-fest

Output: (none — exits 0 on success)

type-fest requires TypeScript >= 5.5 (older majors are pinned to older type-fest versions). Set "strict": true in tsconfig.json to get the full benefit.

json
{
  "compilerOptions": {
    "strict": true,
    "moduleResolution": "bundler",
    "module": "ESNext"
  }
}

Output: (none — exits 0 on success)

Syntax

Each utility is imported by name from the package root. There is no default export.

typescript
import type {
  PartialDeep,
  ReadonlyDeep,
  RequireAtLeastOne,
  Except,
  SetOptional,
  Tagged,
  JsonValue,
} from "type-fest";

Output: (none — exits 0 on success)

Always use import type for type-fest imports — they have no runtime, and verbatimModuleSyntax (or isolatedModules) will warn if you forget.

Why not hand-roll these?

Hand-written deep utilities look fine for simple cases but break on classes, Date, Map, Set, RegExp, arrays of unions, and recursive types. type-fest's versions are battle-tested across millions of downloads and handle those edge cases. The trade-off is one extra dev-dependency — usually worth it for any project beyond a one-file script.

typescript
// Hand-rolled — fine for plain objects, broken for Date/Map/arrays-of-unions
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };

// type-fest — handles Date, Map, Set, RegExp, tuples, classes, unions correctly
import type { PartialDeep } from "type-fest";

Output: (none — exits 0 on success)

Object utilities

These transform object types — adding, removing, or modifying keys. The standard library covers the shallow cases (Partial, Required, Omit); type-fest covers everything else.

PartialDeep<T> and ReadonlyDeep<T>

Recursive versions of Partial and Readonly that descend through every nested object — handling arrays, tuples, Map, Set, and class instances correctly. Reach for these any time you accept a "patch" payload or want a frozen-snapshot type.

typescript
import type { PartialDeep, ReadonlyDeep } from "type-fest";

type Config = {
  server: {
    host: string;
    ports: { http: number; https: number };
  };
  features: { darkMode: boolean; beta: { telemetry: boolean } };
};

// Every nested field is optional
type PartialConfig = PartialDeep<Config>;
const patch: PartialConfig = {
  server: { ports: { https: 8443 } },     // OK — others omitted
};

// Every nested field is readonly
type FrozenConfig = ReadonlyDeep<Config>;
const frozen: FrozenConfig = { /* … */ } as FrozenConfig;
frozen.server.host = "x";                  // Error: readonly
frozen.server.ports.http = 80;             // Error: readonly

Output: (none — exits 0 on success)

SetOptional<T, K> and SetRequired<T, K>

Flip the optionality of a specific subset of keys while leaving the rest alone. The standard library only has whole-shape Partial<T> and Required<T>.

typescript
import type { SetOptional, SetRequired } from "type-fest";

type User = {
  id: number;
  name: string;
  email: string;
  bio?: string;
};

// Make name and email optional too (id stays required, bio stays optional)
type DraftUser = SetOptional<User, "name" | "email">;
const draft: DraftUser = { id: 1 };

// Make bio required (others unchanged)
type CompleteUser = SetRequired<User, "bio">;
const complete: CompleteUser = { id: 1, name: "Alice Dev", email: "alice@example.com", bio: "engineer" };

Output: (none — exits 0 on success)

Except<T, K>

Like the built-in Omit<T, K> but strictly type-checked: K must be an actual key of T. Omit silently accepts non-existent keys (typos) — a TS papercut Except fixes.

typescript
import type { Except } from "type-fest";

type User = { id: number; name: string; email: string };

type NoEmail   = Except<User, "email">;   // OK
type NoEmial   = Except<User, "emial">;   // Error: "emial" is not a key of User

// Built-in Omit lets this slip through silently
type Sloppy    = Omit<User, "emial">;     // No error — but still returns full User

Output: (none — exits 0 on success)

Merge<A, B> and MergeExclusive<A, B>

Merge overrides A's keys with B's where they collide (deep version: Merge<A, B, { recurseIntoArrays: true }>). MergeExclusive produces a union of "A or B but never both" — useful for mutually-exclusive props.

typescript
import type { Merge, MergeExclusive } from "type-fest";

type Base   = { id: number; name: string; role: string };
type Patch  = { name: string | null; role: "admin" };

// Patch keys win
type Merged = Merge<Base, Patch>;
// { id: number; name: string | null; role: "admin" }

// Either tag XOR href — never both
type LinkOrButton =
  MergeExclusive<{ href: string }, { onClick: () => void }>;

const a: LinkOrButton = { href: "/x" };            // OK
const b: LinkOrButton = { onClick: () => {} };     // OK
const c: LinkOrButton = { href: "/x", onClick: () => {} }; // Error

Output: (none — exits 0 on success)

RequireAtLeastOne<T, K> and RequireExactlyOne<T, K>

Express "at least one of these keys must be present" or "exactly one". Common for API filter objects where the caller must supply some search criterion, but the schema has many possible ones.

typescript
import type { RequireAtLeastOne, RequireExactlyOne } from "type-fest";

type SearchInput = {
  q?: string;
  tag?: string;
  author?: string;
  date?: string;
};

// Caller must give us at least one of q | tag | author | date
type Valid = RequireAtLeastOne<SearchInput, "q" | "tag" | "author" | "date">;

const a: Valid = { q: "ts" };                       // OK
const b: Valid = { tag: "react", author: "alicedev" }; // OK
const c: Valid = {};                                 // Error: at least one required

// Exactly one — no combinations
type Single = RequireExactlyOne<SearchInput, "q" | "tag">;
const d: Single = { q: "ts" };                       // OK
const e: Single = { q: "ts", tag: "react" };         // Error: only one

Output: (none — exits 0 on success)

Simplify<T> and Get<T, Path>

Simplify<T> flattens intersections in tooltips/error messages — purely cosmetic but invaluable for library authors. Get<T, Path> reaches into nested objects with a dotted-string path, mirroring lodash's _.get but at the type level.

typescript
import type { Simplify, Get } from "type-fest";

type Messy = { a: string } & { b: number } & { c: boolean };
// Hover shows: { a: string } & { b: number } & { c: boolean }

type Clean = Simplify<Messy>;
// Hover shows: { a: string; b: number; c: boolean }

type Config = { server: { db: { host: string; port: number } } };
type Host = Get<Config, "server.db.host">;   // string
type Bad  = Get<Config, "server.cache.url">; // unknown (path missing)

Output: (none — exits 0 on success)

Branded / nominal types

Branded types attach an invisible "brand" to a primitive so two strings representing different domains (e.g. UserId vs PostId) cannot be mixed up. type-fest exports two flavours — see also the dedicated branded-types article for the broader pattern.

Tagged<T, Brand> and UnwrapTagged<T>

Tagged<T, Brand> is type-fest's runtime-friendly brand: at runtime the value is still a plain T, but TypeScript treats it as a distinct nominal type. UnwrapTagged<T> strips the brand back off.

typescript
import type { Tagged, UnwrapTagged } from "type-fest";

type UserId = Tagged<string, "UserId">;
type PostId = Tagged<string, "PostId">;

function userId(raw: string): UserId {
  return raw as UserId;
}

function getUser(id: UserId) { /* … */ }

const u = userId("u_42");
getUser(u);                       // OK
getUser("u_42");                  // Error: string is not UserId
getUser("p_99" as PostId);        // Error: PostId is not UserId

type Raw = UnwrapTagged<UserId>;  // string

Output: (none — exits 0 on success)

Opaque<T, Brand> (legacy alias of Tagged)

Older versions of type-fest exported Opaque for the same concept; new code should prefer Tagged, but you may still encounter Opaque in existing codebases — they are equivalent.

typescript
import type { Opaque } from "type-fest";

type Email = Opaque<string, "Email">;

Output: (none — exits 0 on success)

JSON-safe types

When you accept arbitrary JSON in an API or a JSON.stringify boundary, type-fest's Json* family captures the exact shape JSON supports — strictly narrower than unknown.

TypeWhat it allows
JsonPrimitivestring | number | boolean | null
JsonValueJsonPrimitive | JsonArray | JsonObject
JsonArrayJsonValue[]
JsonObject{ [key: string]: JsonValue }
typescript
import type { JsonValue, JsonObject } from "type-fest";

function logEvent(payload: JsonObject) {
  console.log(JSON.stringify(payload));
}

logEvent({ user: "alicedev", count: 3 });               // OK
logEvent({ when: new Date() });                         // Error: Date is not JSON
logEvent({ greet: () => "hi" });                        // Error: function not JSON

// Parsing untrusted input
function parseJson(input: string): JsonValue {
  return JSON.parse(input) as JsonValue;                // narrower than `unknown`
}

Output: (none — exits 0 on success)

Promise / async helpers

These small utilities make function signatures more forgiving without sacrificing inference.

Promisable<T> and AsyncReturnType<T>

Promisable<T> accepts a T or a Promise<T> — perfect for plugin callbacks that may be sync or async. AsyncReturnType<T> is Awaited<ReturnType<T>> shortened.

typescript
import type { Promisable, AsyncReturnType } from "type-fest";

type Hook<T> = (input: string) => Promisable<T>;       // sync or async

function runHook<T>(hook: Hook<T>) {
  return Promise.resolve(hook("input"));               // always Promise-wrapped
}

runHook((s) => s.toUpperCase());                       // sync hook OK
runHook(async (s) => s.toUpperCase());                 // async hook OK

async function fetchUser() {
  return { id: 1, name: "Alice Dev" };
}

type User = AsyncReturnType<typeof fetchUser>;
// Same as: Awaited<ReturnType<typeof fetchUser>>
// = { id: number; name: string }

Output: (none — exits 0 on success)

Array / tuple helpers

type-fest provides several utilities for typing tuples, fixed-length arrays, and array transformations — most of which would take a half-page of infer to hand-write.

ReadonlyTuple<T, Length>, FixedLengthArray<T, Length>

Express "exactly N elements of type T".

typescript
import type { ReadonlyTuple, FixedLengthArray } from "type-fest";

type RGB     = FixedLengthArray<number, 3>;
type ConstRGB = ReadonlyTuple<number, 3>;

const c: RGB = [255, 128, 0];                         // OK
const d: RGB = [255, 128];                            // Error: missing element
const e: RGB = [255, 128, 0, 0];                      // Error: too many

Output: (none — exits 0 on success)

LastArrayElement<T>, ArrayTail<T>, ArraySplice<T, …>

Tuple-level array operations — handy when typing variadic functions or transforming function signatures.

typescript
import type { LastArrayElement, ArrayTail } from "type-fest";

type Args = [string, number, boolean];

type Last = LastArrayElement<Args>;     // boolean
type Tail = ArrayTail<Args>;             // [number, boolean]

Output: (none — exits 0 on success)

String helpers

The standard library has Uppercase, Lowercase, Capitalize, Uncapitalize. type-fest extends that with case transformations and template-literal helpers.

CamelCase<S>, KebabCase<S>, SnakeCase<S>, PascalCase<S>

Convert string literal types between casing conventions — useful for autogenerating API client method names from server route strings.

typescript
import type { CamelCase, KebabCase, SnakeCase, PascalCase } from "type-fest";

type A = CamelCase<"user-profile-card">;   // "userProfileCard"
type B = KebabCase<"UserProfileCard">;     // "user-profile-card"
type C = SnakeCase<"UserProfileCard">;     // "user_profile_card"
type D = PascalCase<"user_profile_card">;  // "UserProfileCard"

Output: (none — exits 0 on success)

Split<S, Sep>, Join<T, Sep>, Replace<S, From, To>

String pattern matching at the type level — see template-literal-types for the underlying mechanics.

typescript
import type { Split, Join, Replace } from "type-fest";

type Parts = Split<"a/b/c", "/">;          // ["a", "b", "c"]
type Path  = Join<["a", "b", "c"], "/">;   // "a/b/c"
type New   = Replace<"foo-bar-baz", "-", "_">; // "foo_bar-baz" (first only)

Output: (none — exits 0 on success)

Class-like utilities

Class<T, Arguments> and AbstractClass<T, Arguments>

A type matching "a constructor that produces a T from given args" — far cleaner than new (...args: any[]) => T.

typescript
import type { Class } from "type-fest";

class HttpClient {
  constructor(public baseUrl: string, public timeout: number) {}
}

// A factory that accepts any class whose constructor takes the right args
function build<T>(cls: Class<T, [string, number]>, url: string, timeout: number): T {
  return new cls(url, timeout);
}

const c = build(HttpClient, "https://api.example.com", 5000);
// c: HttpClient

Output: (none — exits 0 on success)

Comparison vs. hand-rolled utilities

A side-by-side of common needs and the trade-off — for trivial cases hand-rolling is fine; for anything touching Date, Map, recursive types, or arrays-of-unions, prefer type-fest.

NeedHand-rolled (works for simple cases)type-fest (recommended)
All keys optional (deep){ [K in keyof T]?: DeepPartial<T[K]> }PartialDeep<T>
All keys readonly (deep)Same pattern with readonlyReadonlyDeep<T>
Make only some keys optionalOmit<T,K> & { [k in K]?: T[k] }SetOptional<T, K>
At-least-one-ofhand-written unionRequireAtLeastOne<T, K>
Brand a primitiveT & { __brand: 'X' }Tagged<T, "X">
JSON shapeunknown (too wide)JsonValue
Async sync unionT | Promise<T>Promisable<T>
String casinghand-built recursive typesCamelCase / SnakeCase / …
Constructor typenew (...args: any[]) => TClass<T, Arguments>
Reach into nested pathmanual indexed access chainGet<T, "a.b.c">

Common pitfalls

  1. Forgetting import type — runtime-only imports of type-fest will emit import 'type-fest' and break under verbatimModuleSyntax. Always import type { ... } from "type-fest".
  2. PartialDeep on classes — class instances are descended into; if you need the instance left intact, mark the field readonly or use a branded wrapper.
  3. Tagged vs Branded confusiontype-fest uses Tagged (the new name); older docs and Opaque are aliases. Pick one and stick to it in a codebase.
  4. Except vs OmitOmit silently allows typos; Except enforces that the key exists. Standardize on Except for safety.
  5. Merge does not deep-merge by default — pass { recurseIntoArrays: true } or use MergeDeep.
  6. RequireAtLeastOne generated union explodes — the resulting union is exponential in K. Keep K small (under 8 keys) or you'll see slow tooltips.
  7. JsonValue vs unknownunknown accepts Date, functions, Map — none of which round-trip through JSON.stringify. Always use JsonValue at JSON boundaries.
  8. Major version pinning to TypeScripttype-fest v5 requires TS ≥ 5.5. Older TS versions need older type-fest releases. Read the release notes when upgrading.
  9. Treeshaking concernstype-fest has no runtime. There is nothing to treeshake; bundle size is unaffected.

Real-world recipes

Typed PATCH payload for a REST endpoint

PartialDeep plus Except produces the perfect "patch any subset of these fields, except the ones the client cannot send" type.

typescript
import type { PartialDeep, Except } from "type-fest";

type User = {
  id: number;                                  // server-set
  createdAt: Date;                             // server-set
  profile: {
    name: string;
    email: string;
    bio?: string;
  };
};

type UserPatch = PartialDeep<Except<User, "id" | "createdAt">>;

async function updateUser(id: number, patch: UserPatch) {
  await fetch(`/users/${id}`, {
    method: "PATCH",
    body: JSON.stringify(patch),
  });
}

await updateUser(42, { profile: { bio: "engineer" } });        // OK
await updateUser(42, { id: 1 });                               // Error: id excluded

Output: (none — exits 0 on success)

Branded IDs to prevent argument mixups

A classic bug in REST/GraphQL backends is passing a PostId where a UserId is expected — both are strings. Tagged makes the compiler reject the mixup.

typescript
import type { Tagged } from "type-fest";

type UserId = Tagged<string, "UserId">;
type PostId = Tagged<string, "PostId">;
type CommentId = Tagged<string, "CommentId">;

// Tiny smart-constructors
const userId    = (s: string) => s as UserId;
const postId    = (s: string) => s as PostId;
const commentId = (s: string) => s as CommentId;

async function getPostsByUser(uid: UserId): Promise<PostId[]> { /* … */ return []; }
async function deleteComment(cid: CommentId): Promise<void> {  /* … */ }

const u = userId("u_42");
const p = postId("p_99");

await getPostsByUser(u);              // OK
await getPostsByUser(p);              // Error: PostId not assignable to UserId
await deleteComment(p);               // Error: PostId not assignable to CommentId

Output: (none — exits 0 on success)

Modeling a settings page with RequireAtLeastOne

Form pages often demand "you must change something to submit". RequireAtLeastOne encodes that rule in the type, catching empty-submit bugs at compile time.

typescript
import type { RequireAtLeastOne } from "type-fest";

type Settings = {
  theme?: "light" | "dark";
  language?: "en" | "es" | "fr";
  notifications?: boolean;
};

type SettingsPatch = RequireAtLeastOne<Settings, "theme" | "language" | "notifications">;

function saveSettings(patch: SettingsPatch) { /* … */ }

saveSettings({ theme: "dark" });                          // OK
saveSettings({ language: "es", notifications: true });    // OK
saveSettings({});                                          // Error: at least one required

Output: (none — exits 0 on success)

Strict JSON config loader

When loading a JSON file you don't fully trust (user's ~/.config/myapp.json), JsonValue plus a Zod schema at the runtime boundary gives end-to-end type safety.

typescript
import { readFile } from "node:fs/promises";
import { z } from "zod";
import type { JsonValue } from "type-fest";

const ConfigSchema = z.object({
  theme: z.enum(["light", "dark"]),
  recentFiles: z.array(z.string()).max(50),
  api: z.object({ url: z.string().url(), token: z.string() }),
});

type Config = z.infer<typeof ConfigSchema>;

async function loadConfig(path: string): Promise<Config> {
  const raw: JsonValue = JSON.parse(await readFile(path, "utf8"));
  return ConfigSchema.parse(raw);
}

Output: (none — exits 0 on success)

Generic factory using Class

A dependency-injection style factory benefits from Class<T, Args> to type-check the constructor arguments at the call site.

typescript
import type { Class } from "type-fest";

class Logger {
  constructor(public name: string, public level: "info" | "warn" | "error") {}
}

class Cache {
  constructor(public ttlMs: number) {}
}

function instantiate<T, A extends unknown[]>(cls: Class<T, A>, ...args: A): T {
  return new cls(...args);
}

const log = instantiate(Logger, "api", "warn");          // Logger
const cache = instantiate(Cache, 60_000);                // Cache
const bad = instantiate(Logger, "api");                  // Error: missing arg

Output: (none — exits 0 on success)

Strongly-typed event bus

Combine Tagged IDs with a Record-keyed payload to type a tiny pub/sub.

typescript
import type { Tagged } from "type-fest";

type Channel<Name extends string, Payload> = Tagged<Name, "Channel"> & { __payload: Payload };

type UserLoggedIn  = Channel<"user.login",  { userId: string; at: Date }>;
type ItemPurchased = Channel<"order.bought", { itemId: string; price: number }>;

type EventMap = {
  "user.login":   { userId: string; at: Date };
  "order.bought": { itemId: string; price: number };
};

function on<K extends keyof EventMap>(name: K, fn: (payload: EventMap[K]) => void) { /* … */ }
function emit<K extends keyof EventMap>(name: K, payload: EventMap[K]) { /* … */ }

on("user.login", (p) => console.log(p.userId));                    // p typed correctly
emit("order.bought", { itemId: "i_1", price: 9.99 });              // OK
emit("order.bought", { itemId: "i_1" });                           // Error: missing price
emit("user.unknown", { userId: "u_1", at: new Date() });           // Error: bad channel

Output: (none — exits 0 on success)