Skip to content

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

Description

@espetro

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:

  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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions