Tsonic GitHub

Runtime Libraries

Tsonic uses a .NET runtime library to support TypeScript semantics.

Overview

Package Purpose Required
Tsonic.Runtime TypeScript language primitives Always

Tsonic.Runtime

TypeScript language features that don't exist in C#.

Union Types

TypeScript unions don't exist in C#:

namespace Tsonic.Runtime
{
    public sealed class Union<T1, T2>
    {
        private readonly object? _value;
        private readonly int _index;

        private Union(object? value, int index)
        {
            _value = value;
            _index = index;
        }

        public static Union<T1, T2> From1(T1 value) => new(value, 0);
        public static Union<T1, T2> From2(T2 value) => new(value, 1);

        public bool Is1() => _index == 0;
        public bool Is2() => _index == 1;

        public T1 As1() => _index == 0 ? (T1)_value! : throw new InvalidOperationException();
        public T2 As2() => _index == 1 ? (T2)_value! : throw new InvalidOperationException();

        public TResult Match<TResult>(Func<T1, TResult> onT1, Func<T2, TResult> onT2) =>
            _index == 0 ? onT1((T1)_value!) : onT2((T2)_value!);

        // Implicit conversions
        public static implicit operator Union<T1, T2>(T1 value) => From1(value);
        public static implicit operator Union<T1, T2>(T2 value) => From2(value);
    }

    // Similar for Union<T1, T2, T3> through Union<T1..T8>
}

Usage mapping:

TypeScript C#
T \| null \| undefined T?
2-8 type unions Union<T1, T2, ...>
9+ type unions object (fallback)

Example:

// TypeScript
function getValue(): string | number {
  return Math.random() > 0.5 ? "hello" : 42;
}
// Generated C#
public static Union<string, double> getValue()
{
    return Math.random() > 0.5 ? "hello" : 42.0;
}

Structural Typing

TypeScript uses structural typing (duck typing). Two types are compatible if they have the same shape.

namespace Tsonic.Runtime
{
    public static class Structural
    {
        public static T? Clone<T>(object? source) where T : new()
        {
            if (source == null) return default;

            var target = new T();
            var sourceType = source.GetType();
            var targetType = typeof(T);

            var targetProperties = targetType.GetProperties()
                .Where(p => p.CanWrite);

            foreach (var targetProp in targetProperties)
            {
                var sourceProp = sourceType.GetProperty(targetProp.Name);
                if (sourceProp != null && sourceProp.CanRead)
                {
                    var sourceValue = sourceProp.GetValue(source);
                    targetProp.SetValue(target, sourceValue);
                }
            }

            return target;
        }

        public static Dictionary<string, object?> ToDictionary(object? source)
        {
            var result = new Dictionary<string, object?>();
            if (source == null) return result;

            var properties = source.GetType().GetProperties();
            foreach (var prop in properties)
            {
                if (prop.CanRead)
                {
                    result[prop.Name] = prop.GetValue(source);
                }
            }
            return result;
        }
    }
}

Example:

// TypeScript - structural compatibility
interface Point {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}

function use2DPoint(p: Point): void {
  /* ... */
}
const p3d: Point3D = { x: 1, y: 2, z: 3 };
use2DPoint(p3d); // OK - structural typing
// Generated C#
use2DPoint(Structural.Clone<Point>(p3d));

Index Signatures

TypeScript index signatures { [key: string]: T }:

namespace Tsonic.Runtime
{
    public class DictionaryAdapter<T>
    {
        private readonly Dictionary<string, object?> _dictionary;

        public DictionaryAdapter(Dictionary<string, object?> dictionary)
        {
            _dictionary = dictionary ?? new Dictionary<string, object?>();
        }

        public T? this[string key]
        {
            get
            {
                if (_dictionary.TryGetValue(key, out var value) && value is T typedValue)
                {
                    return typedValue;
                }
                return default;
            }
            set => _dictionary[key] = value;
        }

        public IEnumerable<string> Keys => _dictionary.Keys;
        public bool ContainsKey(string key) => _dictionary.ContainsKey(key);
    }
}

typeof Operator

JavaScript-style typeof returns different strings than .NET:

namespace Tsonic.Runtime
{
    public static class Operators
    {
        public static string @typeof(object? value)
        {
            if (value == null) return "undefined";
            if (value is string) return "string";
            if (value is double || value is int || value is float || value is long)
                return "number";
            if (value is bool) return "boolean";
            if (value is Delegate) return "function";
            return "object";
        }
    }
}

Dynamic Object

For dynamic property access:

namespace Tsonic.Runtime
{
    public class DynamicObject
    {
        private readonly Dictionary<string, object?> _properties = new();

        public object? this[string key]
        {
            get => _properties.TryGetValue(key, out var value) ? value : null;
            set => _properties[key] = value;
        }

        public bool HasProperty(string key) => _properties.ContainsKey(key);
        public void DeleteProperty(string key) => _properties.Remove(key);
        public IEnumerable<string> GetKeys() => _properties.Keys;
    }
}

Runtime Dependency

Projects include only Tsonic.Runtime:

<PackageReference Include="Tsonic.Runtime" Version="1.0.0" />

Arrays compile to native C# arrays:

// TypeScript
const arr = [1, 2, 3];
// Generated C#
int[] arr = [1, 2, 3];

For dynamic collections, use List explicitly:

import { List } from "@tsonic/dotnet/System.Collections.Generic.js";
const list = new List<number>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

NativeAOT Compatibility

Tsonic.Runtime is fully NativeAOT compatible:

  • Minimal reflection (only for structural cloning with proper annotations)
  • No dynamic dispatch
  • No runtime code generation
  • Trim-safe - all types explicitly referenced

Reflection annotations for AOT:

public static T? Clone<
    [DynamicallyAccessedMembers(
        DynamicallyAccessedMemberTypes.PublicConstructors |
        DynamicallyAccessedMemberTypes.PublicProperties
    )] T
>(object? source) where T : new()

Package Structure

Tsonic.Runtime

tsonic-runtime/
  src/Tsonic.Runtime/
    Union.cs
    Structural.cs
    DictionaryAdapter.cs
    Operators.cs
    IteratorResult.cs
    Tsonic.Runtime.csproj
  tests/Tsonic.Runtime.Tests/

Tsonic.Runtime is published as a NuGet package.