Skip to content

feat(types): add es-toolkit/types module with compile-time type utilities#1818

Draft
dayongkr wants to merge 9 commits into
toss:mainfrom
dayongkr:worktree-es-toolkit-types
Draft

feat(types): add es-toolkit/types module with compile-time type utilities#1818
dayongkr wants to merge 9 commits into
toss:mainfrom
dayongkr:worktree-es-toolkit-types

Conversation

@dayongkr

@dayongkr dayongkr commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Overview

Adds a declaration-only es-toolkit/types entrypoint for compile-time type utilities. v1 ships nine everyday helpers TypeScript lacks natively:

Type What it does
ValueOf<T> Union of an object's value types (value-side keyof)
Simplify<T> Flattens an intersection/mapped type into one readable object
Merge<T, U> Merges two objects; U overrides overlapping keys
SetOptional<T, K> Makes specific keys optional (scoped Partial)
SetRequired<T, K> Makes specific keys required (scoped Required)
Writable<T> Removes readonly from all properties (inverse of Readonly)
NonEmptyArray<T> Array with at least one element ([T, ...T[]])
DeepPartial<T> Recursive Partial for nested object patches; plain objects, arrays/tuples (non-sparse), and Map/Set contents recurse; Date/RegExp/functions pass through
DeepReadonly<T> Recursive Readonly for immutable state; arrays/tuples become readonly, Map/Set become ReadonlyMap/ReadonlySet

Selection rules

"Standard" = TypeScript native types (lib.*.d.ts).

  1. Native already provides it → skip (Partial, Omit, NonNullable, Awaited, Uppercase, …).
  2. No stricter/gap-filling variants — if something similar exists natively, use it (Except, StrictOmit, Exact skipped). Only fill what's genuinely missing.
  3. Stay consistent with the native spec for related utilities (ValueOf mirrors keyof).

Provide it only when TypeScript has nothing comparable, and shape it like the native types.

Packaging

Declaration-only: publishConfig.exports exposes ./types via the types condition (no runtime default), and postbuild strips the empty JS — ships only .d.ts/.d.mts. Use with import type.

Deep utilities: where the special-casing stops

Deep utilities recurse into data containers (plain objects, arrays/tuples, Map/Set) and pass through atomic built-ins that appear in real data shapes (functions, Date, RegExp). Nothing else is special-cased (WeakMap, typed arrays, Blob, DOM types, class instances fall through to the object branch): observed demand for them is ~0, the fallout is mild, and adding a case later is non-breaking.

Tests

Co-located *.spec.ts with Vitest expectTypeOf (existing convention). 9 files, 22 tests, passing under --typecheck. Specs assert intended design decisions only (including negative cases via @ts-expect-error, e.g. sparse arrays stay illegal under DeepPartial).

Remaining TODO

  • Review the test code
  • Review the JSDoc descriptions
  • Add the next high-priority types (by measured demand): JsonValue family → UnionToIntersectionLiteralUnionBrandDeepRequired

Note: the isNonEmpty guard (pairs with NonEmptyArray) belongs in predicate — separate PR.

@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-toolkit Ready Ready Preview, Comment Jul 5, 2026 12:58pm

Request Review

dayongkr and others added 2 commits July 5, 2026 17:20
…ties

Introduce a declaration-only `es-toolkit/types` entrypoint for compile-time
type utilities. v1 ships seven everyday helpers that TypeScript does not provide
natively: ValueOf, Simplify, Merge, SetOptional, SetRequired, Writable, and
NonEmptyArray.

The module is packaged as declaration-only: publishConfig exposes it through the
`types` condition (no runtime `default`), and postbuild removes the empty JS the
build emits, so consumers receive only .d.ts/.d.mts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
attw fails under node10 resolution because it ignores the "exports" map and
the types module has no root shim. Create a declaration-only root shim
(types.d.ts without a runtime types.js) in postbuild, matching how the other
modules are exposed to node10 consumers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recursive Partial for nested object patches (config overrides, mocks). Arrays
recurse into elements without becoming sparse, tuples keep their arity, and
functions/Date/RegExp pass through unchanged. Single type parameter, no options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Map/Set handling: patch-shaped types are JSON-like in practice, and
the extra branches doubled the type for a tail case. The chain now reads
pass-through -> array -> object -> primitive. Support can be added back
non-breakingly if requested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recursive Readonly for immutable state and no-mutation function parameters.
Same skeleton as DeepPartial: arrays/tuples become readonly recursively,
functions/Date/RegExp pass through. Map/Set traversal intentionally omitted —
sampled real-world usage showed 0/23 resolvable Deep* arguments contain them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without these branches DeepReadonly leaves Map/Set mutable (.set/.add survive)
and DeepPartial lets a plain {} pass as a Map while demanding complete values
inside — both break the utility's promise. DeepReadonly folds Map into the
ReadonlyMap check (structural), DeepPartial keeps the mutable/readonly split.
Chains are formatted flat, one case per line, for readability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
merge/toMerged recurse into plain objects and arrays only; Map and Set are
replaced wholesale. Recursing into their contents at the type level let an
incomplete Map replace a complete one through merge, producing values that
lie about their type. Map/Set (and their readonly variants) now pass through,
so they must be provided complete — exactly what merge does with them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dayongkr and others added 2 commits July 5, 2026 21:38
Settle on one broad structural boundary for the deep utilities: containers
(objects, arrays/tuples, Map/Set) recurse, atomic built-ins (functions, Date,
RegExp) pass through — the same traversal cloneDeep performs and the same
boundary DeepReadonly uses. The merge caveat (Map/Set are replaced wholesale
at runtime) is documented on the type instead of narrowing it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One it() per intent. Drop asserts already implied by whole-shape equality
("leaves other keys unchanged"), representation checks (NonEmptyArray's tuple
form), and duplicate happy paths. Deep* specs already followed this rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant