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 JsonValue — type-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 types — Partial, 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 typefor type-fest imports — they have no runtime, andverbatimModuleSyntax(orisolatedModules) 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.
| Type | What it allows |
|---|---|
JsonPrimitive | string | number | boolean | null |
JsonValue | JsonPrimitive | JsonArray | JsonObject |
JsonArray | JsonValue[] |
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.
| Need | Hand-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 readonly | ReadonlyDeep<T> |
| Make only some keys optional | Omit<T,K> & { [k in K]?: T[k] } | SetOptional<T, K> |
| At-least-one-of | hand-written union | RequireAtLeastOne<T, K> |
| Brand a primitive | T & { __brand: 'X' } | Tagged<T, "X"> |
| JSON shape | unknown (too wide) | JsonValue |
| Async sync union | T | Promise<T> | Promisable<T> |
| String casing | hand-built recursive types | CamelCase / SnakeCase / … |
| Constructor type | new (...args: any[]) => T | Class<T, Arguments> |
| Reach into nested path | manual indexed access chain | Get<T, "a.b.c"> |
Common pitfalls
- Forgetting
import type— runtime-only imports of type-fest will emitimport 'type-fest'and break underverbatimModuleSyntax. Alwaysimport type { ... } from "type-fest". PartialDeepon classes — class instances are descended into; if you need the instance left intact, mark the fieldreadonlyor use a branded wrapper.TaggedvsBrandedconfusion —type-festusesTagged(the new name); older docs andOpaqueare aliases. Pick one and stick to it in a codebase.ExceptvsOmit—Omitsilently allows typos;Exceptenforces that the key exists. Standardize onExceptfor safety.Mergedoes not deep-merge by default — pass{ recurseIntoArrays: true }or useMergeDeep.RequireAtLeastOnegenerated union explodes — the resulting union is exponential inK. KeepKsmall (under 8 keys) or you'll see slow tooltips.JsonValuevsunknown—unknownacceptsDate, functions,Map— none of which round-trip throughJSON.stringify. Always useJsonValueat JSON boundaries.- Major version pinning to TypeScript —
type-festv5 requires TS ≥ 5.5. Older TS versions need older type-fest releases. Read the release notes when upgrading. - Treeshaking concerns —
type-festhas 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)