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:

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 noTS in the schema
  • Excludes inherited fields that should remain hidden
  • Applies property overrides defined in the schema
  • Emits optional members with ? and list members with NodeArray<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.json schema drives both Go and TypeScript generation, ensuring nested struct relationships remain synchronized.
  • Run the generator: Execute node --experimental-strip-types _scripts/generate-ts-ast.ts to produce TypeScript interfaces from Go struct definitions.
  • Inheritance mapping: Go struct embedding translates to TypeScript extends clauses via the deriveNodeTsExtends function.
  • Member filtering: The tsInterfaceMembers function 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →