cheat sheet

type-fest

Package-level reference for type-fest on npm — install (dev-only, pure types), TS version requirements, recursion limits, and alternatives.

type-fest

What it is

type-fest is a curated collection of essential 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. Reach for it when you find yourself about to hand-roll PartialDeep, RequireAtLeastOne, Opaque/Tagged brands, JsonValue, Merge, or any of the dozens of common shapes the TS standard library leaves out.

It's the de-facto community standard for TS utility types — alternatives exist (ts-toolbelt, utility-types) but type-fest has the broadest coverage and the most active maintenance.

Install

bash
# Always a devDep — type-fest has no runtime code
npm install -D type-fest
pnpm add -D type-fest
yarn add -D type-fest
bun add -d type-fest

Output: types available under import type { ... } from "type-fest".

ts
import type { PartialDeep, RequireAtLeastOne, Opaque } from "type-fest";

Output: zero impact on bundle size — types are erased at compile time.

Versioning & Node support

  • Current major line is 4.x (released 2023). Major bumps add types that require newer TS features (template literal types, infer extends, recursive conditional types) and may drop older TS support.
  • Requires TypeScript 5.0+ for the current line. Older TS users should pin to ^2 or ^3.
  • Node runtime support is irrelevant — the package contributes nothing at runtime. Always a dev dependency.
  • ESM-only package ("type": "module"), but because consumers only import type, this never affects runtime module resolution.
  • Loose semver — patch and minor releases regularly add new types; only renames/removes happen at majors.

Package metadata

  • Maintainer: Sindre Sorhus (@sindresorhus)
  • Project home: github.com/sindresorhus/type-fest
  • Docs: README on GitHub (no separate docs site — every export is documented inline in its .d.ts)
  • npm: npmjs.com/package/type-fest
  • License: (MIT OR CC0-1.0)
  • First released: 2018
  • Downloads: tens of millions per week — pulled in transitively by most popular npm tooling.

Peer dependencies & extras

type-fest has no peer dependencies and no companion packages. Notes on the surface:

AspectDetail
ImportsAlways use import type { ... } so the line is elided by the compiler. Plain import { ... } from a types-only package works on most bundlers but warns under verbatimModuleSyntax.
SourceEach export is a single .d.ts file under source/. Read them directly when in doubt — they're the docs.
Source.d.ts subpathsSome types have submodules: type-fest/source/conditional-except etc. The top-level type-fest re-exports everything; deep imports are rare and usually unnecessary.
Companion patternsThe library has no "plugins"; if you need a type it doesn't export, copy the source pattern (the .d.ts files are MIT/CC0 — paste freely).

Alternatives

LibraryTrade-off
ts-toolbeltThe other big utility-types library — different API style (more "FP-flavoured", chainable A.Compute<...>). Smaller user base than type-fest; some types are sharper but the codebase is heavier.
utility-typesOlder, less actively maintained — many of its exports are now in TS's standard library (Partial, Required, Omit). Avoid for new projects.
type-plusSmaller, focused on testing-time helpers (assertType, isType). Niche; complements rather than replaces.
Hand-rollingAlways an option — write the type yourself when the case is simple. Reach for type-fest when the type is non-trivial (PartialDeep, RequireAtLeastOne, Get<T, "a.b.c">) or has edge cases (cycles, arrays, tuples).

Common gotchas

  1. Pure types — no runtime. import { PartialDeep } from "type-fest" at runtime imports an empty module. Bundlers like esbuild and Vite usually drop the import; some older Webpack configs keep a no-op entry. Always use import type to make the elision explicit and unambiguous.
  2. Some types require TS 5.0+. Paths<T>, Get<T, Path>, and other recursion-heavy types use infer extends or NoInfer-style patterns introduced in newer TS. Symptom on older TS: Type instantiation is excessively deep and possibly infinite. Either upgrade TS or pin type-fest to a major line that targets your TS version.
  3. Get<T, "a.b.c"> recursion limits. Deeply nested objects (10+ levels) hit TS's recursion depth limit. Symptom: silent unknown instead of the expected type. Restructure the access or break it into two Get calls.
  4. Opaque<T, Tag> vs Tagged<T, Tag>. Opaque was the original API; Tagged is the recommended replacement (cleaner intersection, plays better with structural assignability for plain values). Both still exist — pick Tagged for new code.
  5. JsonValue doesn't include Date. JSON.stringify(new Date()) produces a string, but JsonValue won't accept a Date as input. Wrap with .toISOString() first or use the dedicated Jsonify<T> utility to walk a real object graph.
  6. PartialDeep doesn't recurse into arrays by default. Tuples become Partial-arrays of partials; plain T[] becomes (Partial<T> | undefined)[], which is rarely what you want. The library exposes PartialDeep<T, { recurseIntoArrays: true }> — opt in explicitly.
  7. Merge<A, B> vs Spread<A, B>. Merge overrides keys in A with their B counterparts (last-one-wins, like spread); Spread is also union-aware. Mixing them up produces silent type mismatches. Read the JSDoc of each before picking.

Real-world recipes

The patterns that come up when you actually reach for type-fest in production TS.

Deep partial for state-update patches

The standard Partial<T> is shallow. For PATCH-style state updates, deep partial is the right shape:

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

interface Profile {
  name: string;
  address: {
    street: string;
    city: string;
    country: { code: string; name: string };
  };
}

function applyPatch(state: Profile, patch: PartialDeep<Profile>): Profile {
  // patch can be { address: { country: { code: "US" } } } — every level optional
  return deepMerge(state, patch);
}

For arrays, opt in explicitly:

typescript
type PartialList = PartialDeep<{ items: Item[] }, { recurseIntoArrays: true }>;

RequireAtLeastOne for "discriminated optional"

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

interface SearchInput { name?: string; email?: string; phone?: string }
type ValidSearch = RequireAtLeastOne<SearchInput, "name" | "email" | "phone">;

function search(q: ValidSearch) { /* ... */ }

search({});                  // TS error — at least one required
search({ name: "Alice" });   // OK
search({ email: "a@b.com" }); // OK

Branded types with Tagged

Distinguish IDs from raw strings at the type level — runtime is still a string:

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

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

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

const raw: string = "user-42";
getUser(raw);                          // TS error — not tagged
getUser(raw as UserId);                // OK at the assertion boundary

Pair with zod's .brand() for one-shot validate + tag.

Get<T, "path.to.value"> for deep key extraction

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

interface Config {
  database: { primary: { host: string; port: number } };
}

type Host = Get<Config, "database.primary.host">;   // string
type Port = Get<Config, "database.primary.port">;   // number

Useful for typed config getters; recursion limit is ~10 levels deep before TS gives up.

Jsonify<T> for serialised state

JSON.stringify drops undefined, converts Date to string, drops methods. Jsonify<T> mirrors the actual round-trip type:

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

interface User { id: number; name: string; createdAt: Date; metadata: undefined }

type Serialised = Jsonify<User>;
//    ^? { id: number; name: string; createdAt: string }  (metadata dropped)

SetRequired / SetOptional for fine-grained adjustments

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

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

type CompleteUser = SetRequired<User, "email" | "bio">;   // email and bio required
type DraftUser = SetOptional<User, "id">;                 // id optional

Merge<A, B> for last-wins object combination

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

interface Defaults { color: string; size: "sm" | "md" | "lg" }
interface UserOpts { size?: "xl"; weight?: "bold" }

type Resolved = Merge<Defaults, UserOpts>;
//    ^? { color: string; size?: "xl"; weight?: "bold" }

Note Merge lets B replace the type of any key from A (size widens to "xl" | undefined). Use Spread for union-aware merging.

Except<T, K> — like Omit but stricter

Omit accepts any key string and silently passes if K isn't in T. Except errors if K isn't actually present:

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

interface User { id: number; name: string }
type WithoutId = Except<User, "id">;       // { name: string }
type Typo = Except<User, "ide">;            // TS error — "ide" not in User

Production deployment

type-fest contributes zero to production bundles — it's types-only, erased at compile time. The deployment concerns are:

  1. import type always. Plain import { ... } from a types-only package may leave a no-op import in some bundlers. import type { ... } is unambiguous.
  2. verbatimModuleSyntax: true in tsconfig.json makes import type mandatory. Recommended for new projects.
  3. No runtime dep tree pollution. type-fest has zero deps; doesn't lock you to a particular TS version (unless you use newer utilities).
  4. Lockfile noise. Patch and minor releases land often (new types added). Use ^4.x in devDependencies and accept the churn, or pin exact.

Performance tuning

The performance dimension for type-fest is type-checker performance, not runtime.

Deep recursion ceiling

TypeScript hits a recursion limit at ~50 levels of conditional/mapped depth (config-dependent). Heavy uses of PartialDeep, Get<T, Path>, or Paths<T> can trigger:

python
Type instantiation is excessively deep and possibly infinite.

Mitigations:

  • Split deep types into two levels: Get<T, "a.b.c.d.e">Get<Get<T, "a.b">, "c.d.e">.
  • Replace with bespoke types for hot paths — interface X { a: { b: { c: string } } } and access X["a"]["b"]["c"] directly.
  • Bump noUncheckedIndexedAccess selectively — sometimes the recursion comes from chained narrowing.

tsc --extendedDiagnostics

bash
npx tsc --noEmit --extendedDiagnostics

Output:

text
Files:              312
Lines of Library:   42103
Lines of Definitions: 18712
Lines of TypeScript: 24398
Parse time:         0.84s
Bind time:          0.39s
Check time:         2.71s
Total time:         3.94s

Watch the Total time and Check time. type-fest utilities like Paths, Get, Jsonify for very large object trees can add seconds per compile. If Check time grows after adding a new type-fest utility, swap for a hand-rolled equivalent.

IDE responsiveness

Hover-over-symbol latency degrades with deep type-fest chains. The VS Code TS language server times out at ~10s on heavy types. If hover hangs, simplify the chain.

ESM/CJS interop & bundling

type-fest is ESM-only ("type": "module" in package.json). Because every consumer uses import type, the module system is irrelevant at runtime — the imports are erased.

typescript
// All of these compile to nothing (no runtime artefact)
import type { PartialDeep } from "type-fest";
import type { Tagged, Jsonify } from "type-fest";
import type { Get } from "type-fest";

Bundlers (esbuild, Vite, Rollup, Webpack) all handle this correctly when verbatimModuleSyntax is on. With the older isolatedModules flag, a plain import { ... } from a types-only package warns.

Submodule imports exist:

typescript
import type { Paths } from "type-fest/source/paths";

…but the top-level re-exports everything; deep imports are rarely needed unless tree-shaking compile time (rare).

Version migration guide

type-fest's majors mostly bump the required TypeScript floor.

From → ToHighlights
1.x → 2.0Required TS 4.1+. Added Tagged as the recommended replacement for Opaque.
2.x → 3.0Required TS 4.4+. New PartialDeep options object (was positional).
3.x → 4.0Required TS 5.0+. Added template-literal-heavy types (Paths, Get, Stringified). Dropped older TS support.
4.x → 5.0 (in active dev)Tighter Tagged semantics. Expanded Jsonify to cover more edge cases.

Common migration friction

  • Opaque<T, Tag>Tagged<T, Tag>. The old export still works in 4.x but Tagged is the recommended form. Mechanical search-and-replace.
  • PartialDeep<T, true> (positional second arg) → PartialDeep<T, { recurseIntoArrays: true }> (options object).
  • StringifyPrimitive removal — fold into Jsonify.
  • TS version bumps are the most common breakage. Pin type-fest to the highest line your TS supports:
    • TS 4.x: type-fest 3.x
    • TS 5.0+: type-fest 4.x

Security considerations

type-fest emits no runtime code, so the security surface is minimal. The risks are indirect:

  1. Supply chain. type-fest itself is published by a well-known author (sindresorhus) with 2FA. Risk is comparable to any popular npm package.
  2. No postinstall script. Doesn't execute anything on install.
  3. Hand-rolled eval-like type tricks. Some type-fest utilities use template-literal recursion that the TS compiler treats as expensive. Not a security issue, but a DoS-via-tsc vector if a malicious internal type imports a giant string literal type.
  4. Tagged types don't enforce at runtime. Tagged<string, "UserId"> is erased — a runtime string happily flows where the compiler expected a tagged value. Pair with Zod's .brand() to enforce at the trust boundary.

Testing strategies

type-fest is types-only; you "test" via the type checker.

tsd for type tests

typescript
import { expectType, expectAssignable, expectNotAssignable } from "tsd";
import type { PartialDeep } from "type-fest";

interface User { name: string; address: { city: string } }

expectAssignable<PartialDeep<User>>({});
expectAssignable<PartialDeep<User>>({ address: {} });
expectAssignable<PartialDeep<User>>({ address: { city: "Berlin" } });
expectNotAssignable<PartialDeep<User>>({ address: 42 });

tsd runs as part of CI; type errors become test failures.

@ts-expect-error

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

interface Q { a?: string; b?: string }
const q: RequireAtLeastOne<Q, "a" | "b"> = { a: "x" };  // OK

// @ts-expect-error — none required
const bad: RequireAtLeastOne<Q, "a" | "b"> = {};

A passing TS check confirms the type behaves; if the error stops occurring, the directive itself fails.

Configuration patterns

Single import line per file

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

Collect all type-fest imports at the top — easier to spot the dependency.

Re-export wrapped types

Hide type-fest behind your domain types so consumers don't import it directly:

typescript
// types/util.ts
export type { PartialDeep as DeepPartial } from "type-fest";
export type { Tagged as Branded } from "type-fest";

If you ever swap type-fest for an alternative (or hand-rolled types), only one file changes.

Troubleshooting common errors

  • Type instantiation is excessively deep and possibly infinite. — usually Get<T, Path> or Paths<T> on a deep object. Split into two Get calls or hand-roll.
  • Cannot find module 'type-fest' — devDeps not installed. Run npm ci.
  • The inferred type of '...' cannot be named without a reference to 'type-fest' — happens when a public API returns a type-fest utility. Either re-export the utility from a domain types file, or wrap in a named interface.
  • Tagged<string, 'X'> widens to string — usually because the value passed through JSON.stringify / JSON.parse or via a generic that doesn't preserve the tag. Re-tag at the trust boundary.
  • PartialDeep doesn't recurse into arrays — default behaviour. Pass { recurseIntoArrays: true } as the options object.

Ecosystem integrations

LibraryRelationship
ts-toolbeltAlternative utility-types library — different API style, smaller user base
utility-typesOlder, mostly defunct — many of its exports are now in TS stdlib
type-plusTesting-time helpers (assertType, isType) — complements type-fest
tsdType-level test framework — pairs naturally with type-fest type tests
expect-typeAssertion-style runtime type tests — works with type-fest
zod.brand<>() runtime tagging — pair with type-fest's Tagged at boundaries

When NOT to use this

type-fest is almost always worth the dev dep — but a few edge cases:

  • Tiny single-file scripts. A one-off .ts file doesn't need a utility library. Hand-roll the type.
  • Types that already exist in TS stdlib. Partial, Required, Pick, Omit, Awaited, ReturnType are all built in — don't reach for type-fest equivalents.
  • Bundle-size-sensitive types in published libraries. When publishing a library, depending on type-fest forces consumers to install it too. For library types, hand-roll or inline the type-fest source (it's MIT-licenced) to avoid the dep.
  • Pure type-checker performance issues. If type-fest is causing tsc slowness on a hot codebase, hand-roll the specific utility you need — TS performs better on bespoke types tailored to your data than on general-purpose recursive ones.

See also