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.