#1518·valibot

TS2589: Type instantiation is excessively deep when consuming InferOutput types across package boundaries via .d.ts

Author: espetroCreated Jun 21, 2026Updated Sep 1, 2026
Labelsbug

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)

typescript
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

typescript
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

bash
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:

typescript
// 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:

  1. 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
  2. Valibot's internal type architecture is adjusted to reduce nesting depth in emitted declarations (e.g., flatter generic structures, type alias consolidation); OR
  3. 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:

typescript
// 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

typescript
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).