How to Generate TypeScript Types for Nested Go Structs in typescript-go
To generate TypeScript types for nested Go structs in the typescript-go repository, run the _scripts/generate-ts-ast.ts script, which transforms the central ast.json schema into TypeScript interfaces that mirror Go's struct embedding and inheritance hierarchies.
The microsoft/typescript-go project maintains a single source-of-truth schema (_scripts/ast.json) that describes the TypeScript AST structure. This approach ensures that nested Go structs defined in internal/ast/ast_generated.go automatically produce corresponding TypeScript interfaces, eliminating manual type synchronization between the Go backend and TypeScript frontend.
The Schema-Driven Architecture
Instead of maintaining parallel type definitions, typescript-go uses a centralized JSON schema to drive two parallel code generators:
_scripts/generate-go-ast.tsproducesinternal/ast/ast_generated.gocontaining Go structs with embedded base types_scripts/generate-ts-ast.tsproduces_packages/native-preview/src/ast/ast.generated.tscontaining TypeScript interfaces withextendsclauses
Because both generators consume the same ast.json schema, any changes to nested struct relationships in Go automatically propagate to the TypeScript definitions.
Generating TypeScript Types from the Schema
To regenerate the TypeScript interfaces after modifying Go structs or the AST schema:
# Install dependencies (first run only)
npm install
# Generate TypeScript types from the Go struct definitions
node --experimental-strip-types _scripts/generate-ts-ast.ts
This command writes the output to _packages/native-preview/src/ast/ast.generated.ts, creating TypeScript interfaces that preserve the exact nesting and inheritance relationships defined in the Go source code.
How Nested Go Structs Map to TypeScript Interfaces
The transformation from Go embedding to TypeScript extension relies on two key functions in _scripts/generate-ts-ast.ts.
Inheritance Handling with deriveNodeTsExtends
The deriveNodeTsExtends function computes the extends clause for each generated interface. It expands Go-only base types to ensure that the public Go hierarchy appears correctly in TypeScript:
Go struct embedding:
type QualifiedName struct {
NodeBase
Left EntityName
Right Identifier
}
Generated TypeScript interface:
export interface QualifiedName extends Node, FlowNode, CompositeBase {
readonly kind: SyntaxKind.QualifiedName;
readonly left: EntityName;
readonly right: Identifier;
}
The extends clause (Node, FlowNode, CompositeBase) reproduces the Go embedding hierarchy, flattening nested struct relationships into TypeScript's inheritance model.
Member Selection with tsInterfaceMembers
The tsInterfaceMembers function filters the generated output to match TypeScript's public API surface:
- Removes fields marked with
noTSin the schema - Excludes inherited fields that should remain hidden
- Applies property overrides defined in the schema
- Emits optional members with
?and list members withNodeArray<T>
This filtering ensures that internal Go implementation details do not leak into the public TypeScript types while preserving the structural relationships of nested data.
Working with Generated Types
Factory Functions for Nested Structures
The generator creates typed factory functions that respect the nesting rules. For example, createQualifiedName accepts the nested types as parameters:
export function createQualifiedName(left: EntityName, right: Identifier): QualifiedName {
return new NodeObject(SyntaxKind.QualifiedName, { left, right }) as unknown as QualifiedName;
}
These factories assign fields belonging to Go-only bases to the object's class-level properties rather than the data payload, maintaining consistency with the Go struct memory layout.
Type Guards for Union Types
For union types involving nested structs, the generator produces type guards based on node aliases defined in the schema:
export function isStatement(node: Node): boolean {
return node.kind === SyntaxKind.Block ||
node.kind === SyntaxKind.EmptyStatement ||
node.kind === SyntaxKind.ExpressionStatement ||
// ... additional statement kinds
false;
}
These guards leverage the SyntaxKind discriminant to narrow types across the nested hierarchy.
Key Implementation Files
| Purpose | Path |
|---|---|
| AST Schema (source of truth) | _scripts/ast.json |
| TypeScript Generator | _scripts/generate-ts-ast.ts |
| Schema Parsing Utilities | _scripts/schema.ts |
| Generated TS Output | _packages/native-preview/src/ast/ast.generated.ts |
| Generated Go Structs | internal/ast/ast_generated.go |
| Go Generator | _scripts/generate-go-ast.ts |
Summary
- Single source of truth: The
ast.jsonschema drives both Go and TypeScript generation, ensuring nested struct relationships remain synchronized. - Run the generator: Execute
node --experimental-strip-types _scripts/generate-ts-ast.tsto produce TypeScript interfaces from Go struct definitions. - Inheritance mapping: Go struct embedding translates to TypeScript
extendsclauses via thederiveNodeTsExtendsfunction. - Member filtering: The
tsInterfaceMembersfunction strips Go-specific implementation details while preserving public API structures. - Auto-generated utilities: Factory functions and type guards are emitted alongside interfaces to support the full AST hierarchy.
Frequently Asked Questions
How do I update TypeScript types after changing Go struct definitions?
Modify the ast.json schema or the Go generator in _scripts/generate-go-ast.ts, then run node --experimental-strip-types _scripts/generate-ts-ast.ts. This regenerates _packages/native-preview/src/ast/ast.generated.ts with updated interfaces reflecting your nested struct changes.
Why does the TypeScript interface extend multiple bases when Go uses embedding?
Go achieves inheritance through struct embedding, while TypeScript uses interface extension. The deriveNodeTsExtends function in _scripts/generate-ts-ast.ts flattens Go's embedded base structs (like NodeBase) into a flat extends clause containing all inherited interfaces, preserving the hierarchical relationship in TypeScript's type system.
What happens to Go-specific fields that shouldn't appear in TypeScript?
The tsInterfaceMembers function filters out fields marked with noTS in the schema and removes inherited fields that are implementation details. Only the public API surface suitable for TypeScript consumers gets emitted to ast.generated.ts.
Can I manually edit the generated TypeScript files?
No. The ast.generated.ts file is automatically generated from ast.json. Any manual edits will be overwritten the next time _scripts/generate-ts-ast.ts runs. To modify the output, update the schema or the generator script instead.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →