type-fest
type-fest is a grab-bag of TypeScript utility types that arguably should have been built into the language: Except, Merge, PartialDeep, ReadonlyDeep, SetOptional, RequireAtLeastOne, Simplify, CamelCase and friends for string manipulation, Tagged for opaque types, and ready-made types for real-world JSON files like PackageJson and TsConfigJson. Everything is imported with import type and disappears at compile time, so there is essentially no runtime cost. The value is not that these types are hard to sketch yourself, but that type-fest's versions have survived years of edge cases around unions, optional keys, index signatures, and recursion limits that homegrown DeepPartial implementations routinely get wrong.
The de-facto standard type utility belt for TypeScript, safe to adopt anywhere strict mode and a current compiler are non-negotiable, and worth it for PackageJson and the deep-transform types alone. If you only need one trick, copy-paste it like the README suggests instead of adding a dependency.
Use it if
- You keep hand-rolling the same mapped types (deep partial, deep readonly, make-these-keys-optional) in every project and want versions with the union and index-signature edge cases already fixed
- You need to type actual package.json or tsconfig.json contents: PackageJson and TsConfigJson alone justify the dependency for tooling authors
- You want tagged (opaque) types like UserId that are strings at runtime but not assignable to each other, via the Tagged and UnwrapTagged pair
- You use string-literal key transforms, like typing an API's snake_case response as camelCase with CamelCasedPropertiesDeep instead of writing the recursive template-literal type yourself
- You need one simple type once: the README explicitly invites copy-pasting individual types with no credit required, which keeps a dependency out of your tree and is often the right call
- You are pinned to an older TypeScript: v5.8.0 requires TypeScript 5.9 or newer plus strict: true, and each major tends to raise the floor, so type-fest upgrades can force compiler upgrades across a monorepo
- You need runtime validation: these types vanish at compile time, so validating actual data at a boundary is zod or ajv territory, not type-fest
- Your team is not fluent in advanced TypeScript: compiler errors that pass through Merge, Simplify, or PartialDeep internals are long and cryptic, and heavy use on very large types can noticeably slow type checking and editor feedback
Setup reality
npm install type-fest as a regular or dev dependency and import type what you need; there is no config and effectively nothing shipped to production. The constraints are compiler-side: the package assumes strict: true (types misbehave without it), v5 requires TypeScript >=5.9 and ESM-style resolution, and the README on GitHub documents the development version, so check the npm page for what your installed version actually includes. Types occasionally change behavior between majors in ways semver cannot really express for types, so pin and read release notes when upgrading.
Patterns
Remove keys with Except (strict Omit)omit-keys-strictly
import type {Except} from 'type-fest';
type Post = {id: string; title: string; draft: boolean};
type PublishedPost = Except<Post, 'draft'>;
//=> {id: string; title: string}Unlike built-in Omit, Except errors if the key does not exist on the type, catching typos and stale refactors at compile time.
Merge two types with override semanticsmerge-object-types
import type {Merge} from 'type-fest';
type Defaults = {timeout: number; retries: number; log: boolean};
type UserConfig = {timeout?: string};
type Config = Merge<Defaults, UserConfig>;
//=> {timeout?: string; retries: number; log: boolean}Keys in the second type win, which plain intersection (&) does not do; A & B with conflicting key types silently produces never for that key.
Deeply optional config objectsdeep-partial
import type {PartialDeep} from 'type-fest';
type Settings = {
server: {host: string; port: number};
features: {darkMode: boolean};
};
function applyOverrides(overrides: PartialDeep<Settings>) {
// every level is optional here
}Handles arrays, maps, sets, and readonly variants that naive recursive Partial implementations break on. Options let you control array recursion.
Freeze a type deeply with ReadonlyDeepdeep-readonly
import type {ReadonlyDeep} from 'type-fest';
type State = {user: {name: string}; tags: string[]};
const initial: ReadonlyDeep<State> = {
user: {name: 'ada'},
tags: ['admin'],
};
// initial.tags.push('x') is a compile errorCompile-time only; it does not Object.freeze anything at runtime. Writable and WritableDeep go the other direction when you need to thaw.
Flip specific keys optional or requiredset-keys-optional-or-required
import type {SetOptional, SetRequired} from 'type-fest';
type User = {id: string; email: string; nickname?: string};
type UserDraft = SetOptional<User, 'id'>;
//=> id becomes optional, for pre-insert objects
type UserComplete = SetRequired<User, 'nickname'>;
//=> nickname becomes requiredCleaner than the Omit-and-intersect dance, and the result stays a single object type instead of an ugly intersection in hovers.
Flatten intersections for readable tooltipssimplify-hover-types
import type {Simplify} from 'type-fest';
type Base = {id: string};
type Extra = {name: string};
type Messy = Base & Extra; // hover shows: Base & Extra
type Clean = Simplify<Base & Extra>;
//=> hover shows: {id: string; name: string}Purely cosmetic for the compiler but a big quality-of-life win in editors and error messages. Also useful to force interfaces into plain object shapes.
Opaque ID types with Taggedtagged-opaque-ids
import type {Tagged} from 'type-fest';
type UserId = Tagged<string, 'UserId'>;
type OrderId = Tagged<string, 'OrderId'>;
function getUser(id: UserId) { /* ... */ }
const orderId = 'ord_1' as OrderId;
// getUser(orderId) is a compile error
const userId = 'usr_1' as UserId;
getUser(userId); // okBoth are plain strings at runtime; the tag exists only in the type system. You must cast at the boundary where IDs enter the program.
Type a package.json you read from disktype-package-json
import type {PackageJson} from 'type-fest';
import {readFile} from 'node:fs/promises';
const pkg = JSON.parse(
await readFile('package.json', 'utf8')
) as PackageJson;
const deps = pkg.dependencies ?? {};Covers the real spec including exports maps and workspaces. TsConfigJson does the same for tsconfig files; both are gold for CLI authors.
Type a snake_case API as camelCasecamel-case-api-response
import type {CamelCasedPropertiesDeep} from 'type-fest';
type ApiUser = {
user_id: string;
contact_info: {phone_number: string};
};
type User = CamelCasedPropertiesDeep<ApiUser>;
//=> {userId: string; contactInfo: {phoneNumber: string}}This types the transformed shape only; you still need a runtime camelcase-keys call to actually convert the data. Keep the two in sync.
Require at least one of several keysrequire-at-least-one
import type {RequireAtLeastOne} from 'type-fest';
type Contact = {email?: string; phone?: string; telegram?: string};
type ReachableContact = RequireAtLeastOne<Contact, 'email' | 'phone'>;
const ok: ReachableContact = {phone: '+1555'};
// const bad: ReachableContact = {telegram: '@x'}; // errorRequireExactlyOne and RequireAllOrNone cover the sibling constraints. These are painful to write correctly by hand because of union distribution.
Keep autocomplete on string unions that allow any stringliteral-union-autocomplete
import type {LiteralUnion} from 'type-fest';
type BuiltinTheme = 'light' | 'dark';
function setTheme(theme: LiteralUnion<BuiltinTheme, string>) { /* ... */ }
setTheme('dark'); // autocompleted
setTheme('my-custom'); // still allowedPlain 'light' | 'dark' | string collapses to string and kills IDE suggestions; LiteralUnion works around that known TypeScript limitation.
Constrain values to be JSON-serializablejson-safe-values
import type {JsonValue, Jsonify} from 'type-fest';
function saveToCache(key: string, value: JsonValue) {
localStorage.setItem(key, JSON.stringify(value));
}
type ApiShape = Jsonify<{createdAt: Date; name: string}>;
//=> {createdAt: string; name: string}JsonValue rejects functions, undefined, and class instances at compile time. Jsonify models what a type becomes after a JSON round trip, like Date turning into string.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ts-toolbelt | npm | You want a larger, more academic collection of type-level operations organized as a namespace system. |
| ts-essentials | npm | You want a smaller curated set of essentials with a lower TypeScript version floor. |
| utility-types | npm | You maintain an older codebase already using it; it is stable but sees little new development. |