Skip to content

Runtime error when extending interfaces with index signatures #351

@olaservo

Description

@olaservo

Bug description

There are two related bugs when dealing with interface inheritance and index signatures:

  1. Explicit error (caught): When an interface has both its own index signature AND extends another interface, ts-to-zod throws: "interface with extends and index signature are not supported!"

  2. Silent failure (uncaught): When an interface extends another interface that has an index signature, ts-to-zod generates code that compiles successfully but fails at runtime with TypeError: [schema].extend is not a function

Both issues stem from the same root cause: ts-to-zod doesn't properly handle the Zod schema composition when index signatures are involved in the inheritance chain.

Input

Case 1 - Explicit error (child has index signature):

export interface Clark {
  name: string;
}

export interface Superman extends Clark {
  [key: string]: any;
}

Case 2 - Silent failure (parent has index signature):

export interface Result {
  _meta?: { [key: string]: unknown };
  [key: string]: unknown;
}

export interface PaginatedResult extends Result {
  nextCursor?: string;
}

Expected output

Case 1:

export const clarkSchema = z.object({
  name: z.string()
});

export const supermanSchema = clarkSchema.and(z.record(z.string(), z.any()));

Case 2:

export const resultSchema = z.record(z.string(), z.unknown()).and(z.object({
  _meta: z.record(z.string(), z.unknown()).optional()
}));

export const paginatedResultSchema = resultSchema.and(z.object({
  nextCursor: z.string().optional()
}));

Actual output

Case 1:

// Throws during generation:
Error: interface with `extends` and index signature are not supported!

Case 2:

export const resultSchema = z.record(z.string(), z.unknown()).and(z.object({
  _meta: z.record(z.string(), z.unknown()).optional()
}));

export const paginatedResultSchema = resultSchema.extend({
  nextCursor: z.string().optional()
});

Runtime error when importing Case 2 generated code:

TypeError: resultSchema.extend is not a function
    at <anonymous> (zodded.ts:435:51)

Root Cause

When a base interface has an index signature, ts-to-zod correctly generates it using z.record().and(z.object()). However:

  • Case 1: ts-to-zod has an explicit check that throws an error
  • Case 2: ts-to-zod doesn't recursively check the inheritance chain for index signatures when both interfaces are in the same file, so it incorrectly uses .extend() on the child

The problem is that schemas created with .and() don't have an .extend() method, causing runtime crashes in Case 2.

Note: When the base interface is imported from an external module, ts-to-zod cannot detect the index signature (this would require cross-file type resolution). The fix addresses same-file inheritance chains.

Impact

This affects any codebase using interface inheritance with index signatures, which is a common pattern for extensible API schemas. Case 2 is particularly problematic because the code generation appears to succeed without errors, but crashes at runtime.

In the Model Context Protocol specification, this pattern is used extensively - the Result interface (with index signature) is extended by 13+ different result types, all of which would fail with the current ts-to-zod version.

Versions

  • TypeScript version: 5.6.2
  • Zod version: 4.1.12
  • ts-to-zod version: 5.0.1 (latest)

Note: I've submitted PR #350 which fixes both cases by:

  • Detecting index signatures recursively through the inheritance chain
  • Using .and(z.object()) instead of .extend() for child interfaces when base has index signature
  • Removing the explicit error check and properly handling the pattern instead

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions