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.json
glyph-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-consumer
Error 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> or v.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.ts indirection boundary.
- The same architectural pattern works fine with Zod, whose
z.infer<> produces flatter generic structures in .d.ts emit.
- This affects any library author using Valibot schemas as public API contracts (RPC payloads, database models, configuration objects).
When
v.InferOutput<typeof Schema>types are exported from a library package and consumed by another package via declaration files (.d.ts), TypeScript reportsTS2589: Type instantiation is excessively deep and possibly infiniteat any site that references the exported type through an interface (e.g.,class X implements Interfaceor: Interfacereturn type annotation).Crucially, the same schema and consumer code compile without errors in a single-file setup (no
declaration: true, no cross-package.d.tsboundary). The error is specific to the declaration-emit /.d.tsconsumption path.Reproduction
A complete reproducer is available at: CodeSandbox.
Project structure
glyph-api/src/index.ts(simplified)glyph-consumer/src/index.tsCommands
pnpm install pnpm run check # builds glyph-api, then type-checks glyph-consumerError output
The error points at
class MemoryKeeper implements Keeper.Root Cause Analysis
When
glyph-apicompiles withdeclaration: true, TypeScript emits a.d.tsfile where every Valibot schema is fully materialized as deeply nested generic instantiations:When the consumer references
Batch(viav.InferOutput<typeof BatchSchema>), TypeScript must re-instantiate this entire generic tree from the.d.tsto check structural assignability — exceeding its internal instantiation depth limit.Quantified impact
.d.tssizev.X<Schema<...>>)Expected Behavior
One of the following:
v.Evaluate<T>orv.Flatten<T>) that eagerly collapses deep schema types to plain object types, safe for declaration emit; ORActual Behavior
No built-in utility exists. Users hit
TS2589with no obvious path forward.Workarounds
Workaround 1: Manual type flattening (confirmed working)
Replace
v.InferOutputwith eagerly evaluated plain types at export: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'sSimplifyorJsonifyEnvironment
valibot1.4.1typescript5.8.2and6.0.3-dev(both reproduce)moduleResolutionNodeNextstricttrueexactOptionalPropertyTypestrueAdditional Context
declaration: true, everything in one TS project) compiles cleanly — the issue is specific to the.d.tsindirection boundary.z.infer<>produces flatter generic structures in.d.tsemit.