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
# 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".
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
^2or^3. - Node runtime support is irrelevant — the package contributes nothing at runtime. Always a dev dependency.
- ESM-only package (
"type": "module"), but because consumers onlyimport 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:
| Aspect | Detail |
|---|---|
| Imports | Always 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. |
| Source | Each export is a single .d.ts file under source/. Read them directly when in doubt — they're the docs. |
Source.d.ts subpaths | Some 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 patterns | The 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
| Library | Trade-off |
|---|---|
| ts-toolbelt | The 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-types | Older, less actively maintained — many of its exports are now in TS's standard library (Partial, Required, Omit). Avoid for new projects. |
| type-plus | Smaller, focused on testing-time helpers (assertType, isType). Niche; complements rather than replaces. |
| Hand-rolling | Always 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
- 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 useimport typeto make the elision explicit and unambiguous. - Some types require TS 5.0+.
Paths<T>,Get<T, Path>, and other recursion-heavy types useinfer extendsorNoInfer-style patterns introduced in newer TS. Symptom on older TS:Type instantiation is excessively deep and possibly infinite. Either upgrade TS or pintype-festto a major line that targets your TS version. Get<T, "a.b.c">recursion limits. Deeply nested objects (10+ levels) hit TS's recursion depth limit. Symptom: silentunknowninstead of the expected type. Restructure the access or break it into twoGetcalls.Opaque<T, Tag>vsTagged<T, Tag>.Opaquewas the original API;Taggedis the recommended replacement (cleaner intersection, plays better with structural assignability for plain values). Both still exist — pickTaggedfor new code.JsonValuedoesn't includeDate.JSON.stringify(new Date())produces a string, butJsonValuewon't accept aDateas input. Wrap with.toISOString()first or use the dedicatedJsonify<T>utility to walk a real object graph.PartialDeepdoesn't recurse into arrays by default. Tuples becomePartial-arrays of partials; plainT[]becomes(Partial<T> | undefined)[], which is rarely what you want. The library exposesPartialDeep<T, { recurseIntoArrays: true }>— opt in explicitly.Merge<A, B>vsSpread<A, B>.Mergeoverrides keys inAwith theirBcounterparts (last-one-wins, like spread);Spreadis 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:
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:
type PartialList = PartialDeep<{ items: Item[] }, { recurseIntoArrays: true }>;
RequireAtLeastOne for "discriminated optional"
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:
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
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:
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
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
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:
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:
import typealways. Plainimport { ... }from a types-only package may leave a no-op import in some bundlers.import type { ... }is unambiguous.verbatimModuleSyntax: trueintsconfig.jsonmakesimport typemandatory. Recommended for new projects.- No runtime dep tree pollution. type-fest has zero deps; doesn't lock you to a particular TS version (unless you use newer utilities).
- Lockfile noise. Patch and minor releases land often (new types added). Use
^4.xindevDependenciesand 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:
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 accessX["a"]["b"]["c"]directly. - Bump
noUncheckedIndexedAccessselectively — sometimes the recursion comes from chained narrowing.
tsc --extendedDiagnostics
npx tsc --noEmit --extendedDiagnostics
Output:
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.
// 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:
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 → To | Highlights |
|---|---|
| 1.x → 2.0 | Required TS 4.1+. Added Tagged as the recommended replacement for Opaque. |
| 2.x → 3.0 | Required TS 4.4+. New PartialDeep options object (was positional). |
| 3.x → 4.0 | Required 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 butTaggedis the recommended form. Mechanical search-and-replace.PartialDeep<T, true>(positional second arg) →PartialDeep<T, { recurseIntoArrays: true }>(options object).StringifyPrimitiveremoval — fold intoJsonify.- 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:
- Supply chain. type-fest itself is published by a well-known author (
sindresorhus) with 2FA. Risk is comparable to any popular npm package. - No
postinstallscript. Doesn't execute anything on install. - 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. - Tagged types don't enforce at runtime.
Tagged<string, "UserId">is erased — a runtimestringhappily 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
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
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
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:
// 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.— usuallyGet<T, Path>orPaths<T>on a deep object. Split into twoGetcalls or hand-roll.Cannot find module 'type-fest'— devDeps not installed. Runnpm 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 tostring— usually because the value passed throughJSON.stringify/JSON.parseor via a generic that doesn't preserve the tag. Re-tag at the trust boundary.PartialDeepdoesn't recurse into arrays — default behaviour. Pass{ recurseIntoArrays: true }as the options object.
Ecosystem integrations
| Library | Relationship |
|---|---|
ts-toolbelt | Alternative utility-types library — different API style, smaller user base |
utility-types | Older, mostly defunct — many of its exports are now in TS stdlib |
type-plus | Testing-time helpers (assertType, isType) — complements type-fest |
tsd | Type-level test framework — pairs naturally with type-fest type tests |
expect-type | Assertion-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
.tsfile doesn't need a utility library. Hand-roll the type. - Types that already exist in TS stdlib.
Partial,Required,Pick,Omit,Awaited,ReturnTypeare 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
tscslowness 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
- TypeScript: type-fest — full per-type API and recipes
- TypeScript: utility-types — the TS-builtin baseline
- TypeScript: mapped-conditional-types — how to read
type-festsource - TypeScript: branded-types — companion for
Opaque/Tagged