-
Notifications
You must be signed in to change notification settings - Fork 92
Description
Bug description
There are two related bugs when dealing with interface inheritance and index signatures:
-
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!" -
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