TS2589: Type instantiation is excessively deep when consuming InferOutput types across package boundaries via .d.ts
When v.InferOutput<typeof Schema> types are exported from a library package and consumed by another package via declaration files (.d.ts), TypeScript reports TS2589: Type instantiation is excessively deep and possibly infinite at any site that references the exported type through an interface (e.g., class X implements Interface or : Interface return type annotation).
Crucially, the same schema and consumer code compile without errors in a single-file setup (no declaration: true, no cross-package .d.ts boundary). The error is specific to the declaration-emit / .d.ts consumption path.
Reproduction
A complete reproducer is available at: CodeSandbox.
Project structure
packages/
glyph-api/ # exports BatchSchema + Keeper interface
src/index.ts
package.json # "declaration": true
tsconfig.json
glyph-consumer/ # imports from glyph-api, implements Keeper
src/index.ts
tsconfig.jsonglyph-api/src/index.ts (simplified)
import * as v from "valibot";
// --- schemas (full repro has ~50+ schemas composed together) ---
const MomentSchema = v.union([
v.pipe(v.string(), v.isoDateTime(), v.transform((x) => new Date(x))),
v.pipe(v.instance(Date), v.check((d) => Number.isFinite(d.getTime()))),
]);
// ... RealmIdSchema, GlyphSchema, CapsuleSchema, ChangeSchema, etc.
export const BatchSchema = v.strictObject({
expectedSequence: v.pipe(v.number(), v.safeInteger(), v.minValue(0)),
sequence: v.pipe(v.number(), v.safeInteger(), v.minValue(0)),
mood: v.picklist(["glyph.shift", "blob.shift", "capsule.wake", /* ... */]),
changes: v.array(ChangeSchema), // union of 8+ strictObject schemas
});
export type Batch = v.InferOutput<typeof BatchSchema>;
export type Snapshot = v.InferOutput<typeof SnapshotSchema>;
export interface Keeper {
load(): Promise<Snapshot>;
accept(input: Batch): Promise<BatchReceipt>;
}glyph-consumer/src/index.ts
import { type Batch, type BatchReceipt, type Keeper, type Snapshot } from "@repro/glyph-api";
class MemoryKeeper implements Keeper {
async load(): Promise<Snapshot> {
throw new Error("not needed");
}
async accept(input: Batch): Promise<BatchReceipt> {
return { sequence: input.sequence };
}
}Commands
pnpm install
pnpm run check # builds glyph-api, then type-checks glyph-consumerError output
glyph-consumer/src/index.ts(10,7): error TS2589:
Type instantiation is excessively deep and possibly infinite.The error points at class MemoryKeeper implements Keeper.
Root Cause Analysis
When glyph-api compiles with declaration: true, TypeScript emits a .d.ts file where every Valibot schema is fully materialized as deeply nested generic instantiations:
// Generated .d.ts (excerpt) — 51 KB, 477 lines
export declare const BatchSchema: v.StrictObjectSchema<{
readonly expectedSequence: v.SchemaWithPipe<
readonly [
v.NumberSchema<undefined>,
v.SafeIntegerAction<number, undefined>,
v.MinValueAction<number, 0, undefined>
]
>;
readonly changes: v.ArraySchema<
v.UnionSchema<
readonly [
v.StrictObjectSchema<{
readonly type: v.LiteralSchema<"realm.card.replace", undefined>;
readonly card: v.StrictObjectSchema<{
readonly realmId: v.SchemaWithPipe<
readonly [
v.StringSchema<undefined>,
v.NonEmptyAction<string, `${string} must not be empty.`>,
v.RegexAction<string, `${string} must be plain ASCII.`>
]
>;
// ... dozens more levels
}>;
}>;
]
>;
>;
}, undefined>;When the consumer references Batch (via v.InferOutput<typeof BatchSchema>), TypeScript must re-instantiate this entire generic tree from the .d.ts to check structural assignability — exceeding its internal instantiation depth limit.
Quantified impact
| Metric | Value |
|---|---|
Generated .d.ts size |
51 KB / 477 lines |
Generic type instantiations (v.X<Schema<...>>) |
~286 |
| Schema type references | 506 |
| Deepest single line | 564 characters |
Expected Behavior
One of the following:
- Valibot provides a built-in type utility (e.g.,
v.Evaluate<T>orv.Flatten<T>) that eagerly collapses deep schema types to plain object types, safe for declaration emit; OR - Valibot's internal type architecture is adjusted to reduce nesting depth in emitted declarations (e.g., flatter generic structures, type alias consolidation); OR
- At minimum, official documentation guidance for library authors on how to safely export inferred types.
Actual Behavior
No built-in utility exists. Users hit TS2589 with no obvious path forward.
Workarounds
Workaround 1: Manual type flattening (confirmed working)
Replace v.InferOutput with eagerly evaluated plain types at export:
// glyph-api/src/index.ts
type Simplify<T> = { [K in keyof T]: T[K] } & {};
export type Batch = Simplify<v.InferOutput<typeof BatchSchema>>;
export type Snapshot = Simplify<v.InferOutput<typeof SnapshotSchema>>;This collapses the 51 KB nested generic tree to a ~2 KB plain object type in the .d.ts, and the consumer compiles without error.
Workaround 2: Use type-fest's Simplify or Jsonify
import type { Simplify } from 'type-fest';
export type Batch = Simplify<v.InferOutput<typeof BatchSchema>>;Environment
| Package | Version |
|---|---|
valibot |
1.4.1 |
typescript |
5.8.2 and 6.0.3-dev (both reproduce) |
moduleResolution |
NodeNext |
strict |
true |
exactOptionalPropertyTypes |
true |
Additional Context
- Single-file compilation (no
declaration: true, everything in one TS project) compiles cleanly — the issue is specific to the.d.tsindirection boundary. - The same architectural pattern works fine with Zod, whose
z.infer<>produces flatter generic structures in.d.tsemit. - This affects any library author using Valibot schemas as public API contracts (RPC payloads, database models, configuration objects).
Source: open-circle/valibot