Breakout Generated Types into TS Modules to Avoid Name Collisions
Problem
writeTypeScriptLibrary collects all TSDeclarations from the API and writes them into a single client.d.ts file via formatTSDeclarations. The generated declarations are ambient (no import or export), so they all occupy the global TypeScript namespace. Two Haskell types from different modules that share a name produce a duplicate identifier error.
For example, given two Haskell modules that both define a Status type:
-- Foo.hs
data Status = Active | Inactive
-- Bar.hs
data Result = Ok Status | Err String
data Status = Status { code :: Int, message :: String }
The generated output collides:
// client.d.ts (generated, ambient declarations)
type Status = "Active" | "Inactive";
// ^^^^^^ Duplicate identifier 'Status'.
interface IStatus {
// ^^^^^^^ Duplicate identifier 'IStatus'.
code: number;
message: string;
}
type Status = IStatus;
type Result = IResultOk | IResultErr;
Simply splitting into multiple .d.ts files would not help — ambient declarations are still merged into the global namespace regardless of which file they appear in. The fix requires generating proper ES modules with export statements and import type references between them.
Proposed Solution
Generate one TypeScript module (with export) per Haskell module, with import type statements to resolve cross-module references.
Expected output structure
Instead of a single ambient client.d.ts, the library would produce:
types/Foo.d.ts:
export type Status = "Active" | "Inactive";
types/Bar.d.ts:
import type { Status } from './Foo';
export interface IStatus {
code: number;
message: string;
}
export type Status = IStatus;
export type Result = IResultOk | IResultErr;
The two Status declarations no longer collide because each file is an ES module (due to the export keyword) with its own scope.
What this would require
-
Module provenance on declarations — aeson-typescript's TSDeclaration currently carries the type/interface name but not the originating Haskell module. The library would need some way to associate each declaration with its source module — either by extending TSDeclaration with module metadata, adding a method to the TypeScript class, or accepting module annotations from the consumer.
-
export on all declarations — The current formatTSDeclarations emits ambient declarations (no export). A new formatter or formatting option would need to emit export on each declaration so the output files are proper ES modules.
-
Cross-module import resolution — On the servant-typescript side, writeClientTypes would need to:
- Group declarations by source module
- Build an export index (
Map Identifier ModulePath) from the grouped declarations
- For each module, resolve cross-module references and render relative
import type statements
- Write each module to its own
.d.ts file under the output directory
A ServantTypeScriptOptions field (e.g. typeFileLayout :: TypeFileLayout with SingleFile | PerModule variants) could keep this backwards-compatible.
Workaround
Currently the only workaround is to avoid name collisions at the Haskell level (renaming types or suppressing generation), which doesn't scale.
Breakout Generated Types into TS Modules to Avoid Name Collisions
Problem
writeTypeScriptLibrarycollects allTSDeclarations from the API and writes them into a singleclient.d.tsfile viaformatTSDeclarations. The generated declarations are ambient (noimportorexport), so they all occupy the global TypeScript namespace. Two Haskell types from different modules that share a name produce a duplicate identifier error.For example, given two Haskell modules that both define a
Statustype:The generated output collides:
Simply splitting into multiple
.d.tsfiles would not help — ambient declarations are still merged into the global namespace regardless of which file they appear in. The fix requires generating proper ES modules withexportstatements andimport typereferences between them.Proposed Solution
Generate one TypeScript module (with
export) per Haskell module, withimport typestatements to resolve cross-module references.Expected output structure
Instead of a single ambient
client.d.ts, the library would produce:types/Foo.d.ts:types/Bar.d.ts:The two
Statusdeclarations no longer collide because each file is an ES module (due to theexportkeyword) with its own scope.What this would require
Module provenance on declarations —
aeson-typescript'sTSDeclarationcurrently carries the type/interface name but not the originating Haskell module. The library would need some way to associate each declaration with its source module — either by extendingTSDeclarationwith module metadata, adding a method to theTypeScriptclass, or accepting module annotations from the consumer.exporton all declarations — The currentformatTSDeclarationsemits ambient declarations (noexport). A new formatter or formatting option would need to emitexporton each declaration so the output files are proper ES modules.Cross-module import resolution — On the
servant-typescriptside,writeClientTypeswould need to:Map Identifier ModulePath) from the grouped declarationsimport typestatements.d.tsfile under the output directoryA
ServantTypeScriptOptionsfield (e.g.typeFileLayout :: TypeFileLayoutwithSingleFile | PerModulevariants) could keep this backwards-compatible.Workaround
Currently the only workaround is to avoid name collisions at the Haskell level (renaming types or suppressing generation), which doesn't scale.