Tsonic GitHub

Emit Phase

The Emit phase generates TypeScript declaration files from the emission plan.

Emitters

Emitter Output Purpose
ExtensionsEmitter __internal/extensions/index.d.ts Extension method buckets
InternalIndexEmitter <Namespace>/internal/index.d.ts Full type declarations
FacadeEmitter <Namespace>.d.ts Public facade with re-exports
FamilyIndexEmitter families.json Multi-arity family index
MetadataEmitter <Namespace>/internal/metadata.json CLR semantics
BindingEmitter <Namespace>/bindings.json CLR↔TS name mappings
ModuleStubEmitter <Namespace>.js Runtime stubs (throw if executed)

InternalIndexEmitter

File: Emit/InternalIndexEmitter.cs

Generates the full type declarations:

public static void Emit(BuildContext ctx, EmissionPlan plan, string outputDirectory)
{
    foreach (var nsOrder in plan.EmissionOrder.Namespaces)
    {
        var content = GenerateNamespaceDeclaration(ctx, plan, nsOrder);
        var outputName = NamespacePathMapper.GetOutputName(nsOrder.Namespace, ctx);
        var outputFile = Path.Combine(outputDirectory, outputName, "internal", "index.d.ts");
        File.WriteAllText(outputFile, content);
    }
}

Output structure:

// Generated by tsbindgen
// Namespace: System.Collections.Generic

// Branded primitive imports
import type { int, long } from '@tsonic/core/types.js';

// Cross-namespace imports
import * as System_Internal from '../../System/internal/index.js';
import type { IEnumerable_1 } from '../../System.Collections/internal/index.js';

// Type declarations
export interface List_1$instance<T> {
    readonly count: int;
    add(item: T): void;
    // ...
}

export declare const List_1: {
    new <T>(): List_1<T>;
    new <T>(capacity: int): List_1<T>;
};

export interface __List_1$views<T> {
    As_IEnumerable_1(): IEnumerable_1<T>;
}

export type List_1<T> = List_1$instance<T> & __List_1$views<T>;

MultiArityAliasEmit

File: Emit/MultiArityAliasEmit.cs

Generates sentinel-ladder type aliases for multi-arity families:

public static class MultiArityAliasEmit
{
    public static string EmitFamilyAlias(
        MultiArityFamily family,
        TypeNameResolver resolver,
        BuildContext ctx,
        string currentNamespace)
    {
        // Emit sentinel and facade type alias
        // Uses sentinel-ladder pattern with nested constraint guards
    }
}

Output structure:

// Sentinel symbol
declare const __unspecified: unique symbol;
export type __ = typeof __unspecified;

// Multi-arity facade
export type Action<T1 = __, T2 = __> =
  [T1] extends [__] ? Internal.Action :
  [T2] extends [__] ? Internal.Action_1<T1> :
  Internal.Action_2<T1, T2>;

Key methods:

Method Purpose
EmitFamilyAlias Main entry point for family emission
BuildNestedConstraintCheck Generates nested [T] extends [C] guards
IsSameNamespace Determines if type needs Internal. prefix

FamilyIndexEmitter

File: Emit/FamilyIndexEmitter.cs

Emits families.json - a canonical index of multi-arity families for cross-package imports:

public static class FamilyIndexEmitter
{
    public static void Emit(
        BuildContext ctx,
        IReadOnlyDictionary<string, MultiArityFamily> families,
        string outputDirectory)
    {
        // Write families.json with family metadata
    }
}

Output structure:

{
  "System.Action": {
    "stem": "Action",
    "namespace": "System",
    "minArity": 0,
    "maxArity": 16,
    "isDelegate": true
  }
}

Used by ImportPlanner to resolve multi-arity imports across packages.

FacadeEmitter

File: Emit/FacadeEmitter.cs

Generates the public-facing facade:

public static void Emit(BuildContext ctx, EmissionPlan plan, string outputDirectory)
{
    foreach (var nsOrder in plan.EmissionOrder.Namespaces)
    {
        var ns = nsOrder.Namespace;
        var content = GenerateFacade(ctx, plan, ns);
        var outputName = NamespacePathMapper.GetOutputName(nsOrder.Namespace, ctx);
        var outputFile = Path.Combine(outputDirectory, $"{outputName}.d.ts");
        File.WriteAllText(outputFile, content);
    }
}

Output structure:

// Generated by tsbindgen
// Namespace: System.Collections.Generic
// Facade - Public API Surface (curated exports, no export *)

import * as Internal from './System.Collections.Generic/internal/index.js';

// Cross-namespace type imports for constraints
import type { IEquatable_1 } from './System/internal/index.js';

// Public API exports (curated - no export * to prevent $instance/$views leakage)
// Value re-exports for classes (TypeScript re-exports both value AND type)
export { List_1 as List } from './System.Collections.Generic/internal/index.js';
export { Dictionary_2 as Dictionary } from './System.Collections.Generic/internal/index.js';

// Type aliases for interfaces
export type IEnumerable<T> = Internal.IEnumerable_1<T>;
export type IList<T> = Internal.IList_1<T>;

Type Emission Pattern

Classes and structs emit with a three-part pattern:

1. Instance Interface

export interface List_1$instance<T> {
    readonly count: int;
    add(item: T): void;
    remove(item: T): boolean;
    clear(): void;
    // ... instance members
}

2. Value Export (const)

export declare const List_1: {
    // Constructors
    new <T>(): List_1<T>;
    new <T>(capacity: int): List_1<T>;
    new <T>(collection: IEnumerable_1<T>): List_1<T>;

    // Static members (if any)
};

3. Views Interface

export interface __List_1$views<T> {
    As_IEnumerable_1(): IEnumerable_1<T>;
    As_ICollection_1(): ICollection_1<T>;
    As_IList_1(): IList_1<T>;
}

4. Type Alias

export type List_1<T> = List_1$instance<T> & __List_1$views<T>;

ClassPrinter

File: Emit/Printers/ClassPrinter.cs

Prints individual type declarations:

public static string Print(TypeSymbol type, TypeNameResolver resolver, ...)
{
    return type.Kind switch
    {
        TypeKind.Class => PrintClass(type, resolver, ...),
        TypeKind.Interface => PrintInterface(type, resolver, ...),
        TypeKind.Struct => PrintStruct(type, resolver, ...),
        TypeKind.Enum => PrintEnum(type, resolver, ...),
        TypeKind.Delegate => PrintDelegate(type, resolver, ...),
        TypeKind.StaticNamespace => PrintStaticClass(type, resolver, ...),
    };
}

TypeRefPrinter

File: Emit/Printers/TypeRefPrinter.cs

Prints type references:

public static string Print(TypeReference typeRef, TypeNameResolver resolver, ...)
{
    return typeRef switch
    {
        NamedTypeReference named => PrintNamed(named, resolver),
        GenericParameterReference gp => gp.Name,
        ArrayTypeReference arr => $"{Print(arr.ElementType, resolver)}[]",
        PointerTypeReference ptr => $"ptr<{Print(ptr.ElementType, resolver)}>",
        ByRefTypeReference byref => $"{{ value: ref<{Print(byref.ElementType, resolver)}> }}",
        // ...
    };
}

TypeNameResolver

File: Emit/TypeNameResolver.cs

Resolves type names with proper qualification:

public string Resolve(TypeReference typeRef)
{
    // Same namespace: use simple name
    // Different namespace: use qualified name (Namespace_Internal.Type)
    // Imported type: use import alias
}

Nullable Reference Type (NRT) Emission

NRT emission follows an asymmetric position-based strategy:

Output Positions

Returns, properties, and fields emit | undefined for nullable references:

// C#: public string? Name { get; }
readonly name: string | undefined;

// C#: public T? GetItem<T>() where T : class
getItem<T>(): T | undefined;

Input Positions

Parameters are always non-nullable (no | undefined):

// C#: public void Process(string? value)
process(value: string): void;

Generic Type Arguments in Outputs

When a nullable generic type arg appears in an output position, the nullability propagates:

// C#: public List<string?>? Items { get; }
readonly items: List_1<string | undefined> | undefined;

Implementation

NRT information is extracted from C# nullable attributes (NullableAttribute, NullableContextAttribute) in the reflection phase and tracked in the model. The emit phase checks TypeReference.IsNullableReference and adds | undefined only for output positions.

Class Flattening Emission

When --flatten-class is specified, static classes emit top-level functions instead of an abstract class:

// C#
public static class Console {
    public static void WriteLine(string value);
    public static string ReadLine();
}

With --flatten-class "System.Console":

// TypeScript (flattened)
export function writeLine(value: string): void;
export function readLine(): string;

Without flattening:

// TypeScript (default)
export abstract class Console {
    static writeLine(value: string): void;
    static readLine(): string;
}

Flattened exports are placed in the facade file for the namespace.