Skip to content

Deepstaging.Roslyn.TypeScript

Fluent builders for generating TypeScript source code from C#.

See also: C# Emit | Types | Expressions

What is this?

This satellite package generates TypeScript source code using the same fluent, immutable builder patterns as the C# Emit layer. It's a C# library — you write C#, it outputs TypeScript strings.

Where the C# layer produces Roslyn CompilationUnitSyntax, the TypeScript layer produces raw string output — there's no TypeScript AST in .NET. Optional tsc validation and dprint formatting compensate for this at test time.

The package is organized into three layers:

Layer Namespace Purpose
Emit Emit Fluent builders (TsTypeBuilder, TsMethodBuilder, etc.) and emit pipeline
Types TsTypeRef + Types Composable type references, unions, intersections, utility types
Expressions TsExpressionRef + Expressions Composable expressions, optional chaining, expression factories

Installation

dotnet add package Deepstaging.Roslyn.TypeScript --prerelease

Quick Start

using Deepstaging.Roslyn.TypeScript;
using Deepstaging.Roslyn.TypeScript.Emit;

var result = TsTypeBuilder.Class("UserService")
    .Exported()
    .Extends("BaseService")
    .AddField("users", "Map<string, User>", f => f
        .WithAccessibility(TsAccessibility.Private)
        .AsReadonly()
        .WithInitializer("new Map()"))
    .AddMethod("getUser", m => m
        .Async()
        .AddParameter("id", "string")
        .WithReturnType("Promise<User | undefined>")
        .WithBody(b => b
            .AddReturn("this.users.get(id)")))
    .Emit();

if (result.TryValidate(out var valid))
{
    string tsCode = valid.Code; // TypeScript source code string
}

This generates:

// <auto-generated/>

export class UserService extends BaseService {
  private readonly users: Map<string, User> = new Map();

  async getUser(id: string): Promise<User | undefined> {
    return this.users.get(id);
  }
}

Mapping from C# Emit

If you're familiar with the C# Emit layer, here's how concepts map:

C# Emit TypeScript Emit Difference
TypeBuilder TsTypeBuilder Adds interface, type alias, enum, const enum
MethodBuilder TsMethodBuilder Adds async, generators, optional methods
PropertyBuilder TsPropertyBuilder Adds ? optional, getter/setter bodies
FieldBuilder TsFieldBuilder Adds #private, declare
ConstructorBuilder TsConstructorBuilder Adds parameter properties, super()
BodyBuilder TsBodyBuilder Adds for...of, for...in, const/let
TypeRef TsTypeRef Adds unions, intersections, tuples, keyof, template literals
ExpressionRef TsExpressionRef Adds ?., ??, as, satisfies, ===, spread
OptionalEmitValidEmit TsOptionalEmitTsValidEmit Same pattern, string output instead of syntax tree
EmitOptions TsEmitOptions Adds tsc validation, dprint formatting

Key Differences

  1. Export vs. Accessibility: TypeScript uses export for module visibility. Use .Exported() instead of .WithAccessibility(Accessibility.Public) at the type level.
  2. No namespaces: TypeScript uses ES modules. Use .AddImport() for dependencies.
  3. Union/Intersection types: TypeScript has first-class union (A | B) and intersection (A & B) types — use TsTypeRef.Union(...) and TsTypeRef.Intersection(...).
  4. Optional members: TypeScript has ? syntax — use .AsOptional() on properties, parameters, and methods.
  5. Parameter properties: TypeScript's constructor shorthand (constructor(public name: string)) — use .AsParameterProperty().
  6. #private fields: ES private field syntax — use .AsEsPrivate().
  7. String output: No Roslyn syntax tree — you get a string. Use tsc validation in tests if you need compile-time guarantees.

Pages

  • EmitTsTypeBuilder, member builders, TsBodyBuilder, TsEmitOptions, emit pipeline
  • TypesTsTypeRef, specialized type refs, utility type refs
  • ExpressionsTsExpressionRef, expression factories
  • TestingTsTestBase, option presets, assertions, VerifyEmit snapshots
  • Integration — Real-world usage: analyzer + code fix, MSBuild extraction, CLI tool