Tsonic GitHub
Edit on GitHub

Type Mappings

How CLR types map to TypeScript declarations.

Primitive Types

CLR primitives map to type aliases from @tsonic/core/types.js:

CLR Type TypeScript Underlying
System.SByte sbyte number
System.Byte byte number
System.Int16 short number
System.UInt16 ushort number
System.Int32 int number
System.UInt32 uint number
System.Int64 long number
System.UInt64 ulong number
System.Single float number
System.Double double number
System.Decimal decimal number
System.Char char string & { __brand: "char" }
System.Boolean bool boolean & { __brand: "bool" }
System.String string (native)
System.IntPtr nint number
System.UIntPtr nuint number

Why Simple Aliases?

Numeric types are simple number aliases because TypeScript's structural typing doesn't enforce numeric bounds at runtime. Tsonic enforces numeric correctness at compile time via a proof system:

const age: int = 42 as int;    // Tsonic validates 42 fits in Int32
const temp: float = 98.6 as float; // Tsonic validates for Single

The char and bool types remain branded for semantic distinction.

Generic Types

Generic type names include arity suffix:

CLR TypeScript
List<T> List_1<T>
Dictionary<K,V> Dictionary_2<K, V>
Tuple<T1,T2,T3> Tuple_3<T1, T2, T3>
Action Action
Action<T> Action_1<T>
Func<TResult> Func_1<TResult>
Func<T,TResult> Func_2<T, TResult>

Friendly Aliases

Facades export friendly aliases without arity suffix:

// Both work:
import { List_1 } from "@tsonic/dotnet/System.Collections.Generic.js";
import { List } from "@tsonic/dotnet/System.Collections.Generic.js";  // Alias

Type Kinds

CLR Kind TypeScript Pattern
Class interface + const
Struct interface + const
Interface interface
Enum const enum
Delegate type (callable)
Static class abstract class

Class/Struct Pattern

Classes and structs emit as interface + const:

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

// Value export (constructors + statics)
export declare const List_1: {
    new <T>(): List_1<T>;
    new <T>(capacity: int): List_1<T>;
};

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

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

Interface Pattern

export interface IEnumerable_1$instance<T> {
    getEnumerator(): IEnumerator_1<T>;
}

export type IEnumerable_1<T> = IEnumerable_1$instance<T>;

Enum Pattern

export const enum ConsoleColor {
    Black = 0,
    DarkBlue = 1,
    DarkGreen = 2,
    // ...
}

Delegate Pattern

Delegates emit as callable types:

export type Action_1<T> = ((arg: T) => void) & Action_1$instance<T> & __Action_1$views<T>;
export type Func_2<T, TResult> = ((arg: T) => TResult) & Func_2$instance<T, TResult>;

This allows arrow functions:

const fn: Func_2<int, string> = (x) => x.toString();

Static Class Pattern

export abstract class Console {
    static writeLine(value: string): void;
    static readLine(): string;
    // No constructor - abstract
}

Special Types

CLR TypeScript
void void
object unknown
dynamic unknown
T* (pointer) ptr<T>

ref/out/in Parameters

Parameter modifiers (ref, out, in) are tracked in the bindings manifest (<Namespace>/bindings.json), not the TypeScript declarations:

// TypeScript declaration (no visible difference)
function tryParse(value: string, result: int): bool;
// bindings.json tracks the modifier (simplified example)
{
  "namespace": "System",
  "types": [
    {
      "clrName": "System.Int32",
      "methods": [
        {
          "clrName": "TryParse",
          "parameterModifiers": [
            { "index": 1, "modifier": "out" }
          ]
        }
      ]
    }
  ]
}

This approach is used because:

  1. TypeScript has no concept of by-reference parameters
  2. The Tsonic compiler needs this info for correct C# interop
  3. Runtime behavior differs for ref/out/in (ABI concern)

See Output Files for manifest structure.

Nullable Types

Value Types (Nullable)

CLR TypeScript
int? int \| null
bool? bool \| null
T? where T: struct T \| null

Nullable Reference Types (NRT)

C# nullable reference types are handled based on position:

Output positions (returns, properties, fields) emit | undefined for nullable:

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

// C#: public List<string?>? GetItems()
getItems(): List_1<string | undefined> | undefined;

Input positions (parameters) are always non-nullable in TypeScript:

// C#: public void Process(string? value)
process(value: string): void;  // No undefined - caller must provide value

This asymmetric approach:

  1. Preserves null-safety guarantees from C# analysis
  2. Avoids unnecessary undefined checks for callers
  3. Matches TypeScript's strict null checks semantics

Arrays

CLR TypeScript
T[] T[]
T[,] T[][]
ReadOnlySpan<T> ReadOnlySpan_1<T>
Span<T> Span_1<T>

Primitive Lifting in Generic Type Arguments

For generic type arguments, tsbindgen emits CLR type names directly instead of TS primitive aliases:

// Value positions use TS aliases for ergonomics
function add(a: int, b: int): int;

// Generic type arguments use CLR names
type List = List_1<Int32>;  // Not List_1<int>
tryFormat(destination: Span_1<Char>): boolean;  // Not Span_1<char>

This ensures:

  1. CLR type identity is preserved in type-level positions
  2. Generic constraints are satisfied without runtime type inference
  3. Tsonic compiler can enforce numeric correctness at compile time