skip to content

type-fest — Community utility types for TypeScript

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.

15 min read 25 snippets deep dive

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.

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

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

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.

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

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

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.

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.

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.

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.

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.

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.

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

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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)