C# Language Support

This page lists the C# language features Transpose compiles, by language version, with examples. For the features it rejects, and the errors it reports for them, see Unsupported Features.

Language version

Transpose compiles C# 14. The version is not a project setting: the Transpose.Build.Target SDK overwrites LangVersion with the one version the compiler supports, and tps ignores the value in the .csproj. The IDE and the compiler therefore always analyse the same C#, and a project cannot opt into an older or newer language.

The SDK also defines the TRANSPOSE preprocessor symbol, so source shared with a desktop .NET project can branch on it:

#if TRANSPOSE
    Console.WriteLine("running in the browser");
#else
    Console.WriteLine(File.ReadAllText("settings.json"));
#endif

The translator's test suite checks each feature on this page by compiling a snippet, running the JavaScript on Node, and comparing the output with the same program run on native .NET.

Feature matrix

The tables list language features by the C# version that introduced them. A feature marked No fails the build with an error; the Unsupported Features page shows each error.

C# 1 – 6

Feature Supported
Classes, structs, interfaces, enums, delegates Yes
Inheritance, virtual / override / abstract / sealed, base calls Yes
Properties, indexers (including multi-parameter), events, multicast delegates Yes
Operator overloading and user-defined conversions Yes
Exceptions: try / catch / finally, throw Yes
foreach, using, lock on an object Yes
ref and out parameters Yes
goto to a label Yes
goto case / goto default No
Generics, constraints, generic methods Yes
Nullable value types (int?) and lifted operators Yes
Anonymous methods, lambdas, closures Yes
Lambdas with ref / out / in parameters No
Iterators (yield return / yield break) Yes
Partial types, static classes, extension methods Yes
LINQ query syntax (from, where, let, join, join … into, group … into, orderby) Yes
Anonymous types, var, object and collection initializers Yes
dynamic Partly — see below
async / await Yes — see Async Support
String interpolation, nameof, ?., expression-bodied members Yes
Auto-property initializers, getter-only properties, index initializers Yes
Exception filters (catch … when) Yes
using static Yes
checked integer arithmetic No
unsafe, pointers, fixed, sizeof No
stackalloc Into a Span<T> only — see below
extern / [DllImport] native interop No

C# 7 – 9

Feature Supported
Tuples and deconstruction Yes
Pattern matching: type, constant, relational, logical, property patterns Yes
Positional patterns, including a hand-written Deconstruct Yes — see below
Local functions, static local functions Yes
out var, discards, throw expressions, default literal Yes
in parameters, readonly struct, readonly members, private protected Yes
Non-trailing named arguments Yes
ref locals and ref returns Yes — see below
ref struct (declared in your project) Yes, emitted as an ordinary struct
Nullable reference types Yes (compile-time only)
Switch expressions Yes
using declarations, ??= Yes
Indices and ranges (^1, 1..^1) on arrays, strings, spans, lists and any type with an int indexer and a Length or Count Yes
Default interface members No
Records, init setters, with expressions Yes
Target-typed new and target-typed conditionals Yes
Covariant return types Yes
Static anonymous functions, lambda discard parameters Yes
Module initializers, attributes on local functions Yes
Top-level statements No
Native-sized integers (nint / nuint) No
Async streams (IAsyncEnumerable<T>, await foreach) and await using No

C# 10 – 12

Feature Supported
Record structs Yes
File-scoped namespaces Yes
Extended property patterns ({ A.B: 1 }) Yes
Constant interpolated strings Yes
Mixed declaration and assignment in deconstruction Yes
Lambda improvements (natural type, explicit return type, attributes) Yes
Global using directives No
Raw string literals Yes
List patterns (arrays, strings, spans, lists) Yes
required members Yes
Auto-default structs Yes
Checked user-defined operators Yes
Generic attributes Yes
file-local types Yes
Static abstract / static virtual interface members (generic math) No
UTF-8 string literals ("abc"u8) Yes
Pattern matching a Span<char> against a string constant Yes
Primary constructors on classes and structs Yes
Collection expressions, including spreads ([1, .. other]) Yes
Default values for lambda parameters Yes
ref readonly parameters Yes
using aliases for any type Yes
Inline arrays ([InlineArray]) No

C# 13 – 14

Feature Supported
params collections (params List<T>, params IEnumerable<T>) Yes
params ReadOnlySpan<T> / params Span<T> Yes
First-class span conversions (array or string to a span) Yes — see below
\e escape sequence Yes
Implicit index access ([^1] = …) in object initializers Yes
System.Threading.Lock No — lock on a plain object works
field keyword in property accessors Yes
Unbound generic types in nameof (nameof(List<>)) Yes
Partial properties Yes
Null-conditional assignment (a?.B = 1) Yes
Extension blocks (extension(T source) { … }) No — classic this extension methods work
Modifiers on simple lambda parameters ((ref x) => …) No — same restriction as other ref lambda parameters
User-defined compound assignment and increment operators (operator +=, operator ++) Yes

Examples

Each example below is a complete program. The output shown is the output of the compiled JavaScript, and it matches the output of the same program on .NET.

Records, primary constructors and pattern matching

Program.cs
using System;
using System.Linq;

public record Point(int X, int Y)
{
    public double Length => Math.Sqrt(X * X + Y * Y);
}

public readonly record struct Money(decimal Amount, string Currency);

public abstract class Shape { public abstract double Area(); }
public sealed class Circle(double r) : Shape { public override double Area() => Math.PI * r * r; }
public sealed class Rect(double w, double h) : Shape { public override double Area() => w * h; }

public class Program
{
    static string Describe(object o) => o switch
    {
        null => "null",
        int n when n < 0 => "negative int",
        int n => $"int {n}",
        Point { X: 0, Y: 0 } => "origin",
        Point(var x, var y) => $"point {x},{y}",
        string { Length: > 5 } s => $"long string {s}",
        int[] and [1, .., var last] => $"array ending in {last}",
        _ => "something else",
    };

    public static void Main()
    {
        var p = new Point(3, 4);
        var q = p with { Y = 0 };
        Console.WriteLine($"{p} {q} {p.Length} {p == new Point(3, 4)}");
        Console.WriteLine(new Money(12.50m, "EUR"));

        Shape[] shapes = [new Circle(1), new Rect(2, 3)];
        Console.WriteLine(shapes.Sum(s => s.Area()).ToString("F2"));

        foreach (var o in new object[] { null, -1, 7, new Point(0, 0), new Point(1, 2), "abcdefg", new[] { 1, 2, 3 }, 1.5 })
            Console.WriteLine(Describe(o));

        var (a, b) = (1, "two");
        Console.WriteLine($"{a} {b}");

        int[] arr = [1, 2, 3, .. new[] { 4, 5 }];
        Console.WriteLine(string.Join(",", arr[1..^1]));

        string? maybe = null;
        maybe ??= "default";
        Console.WriteLine(maybe?.ToUpper());

        var raw = """
            {"name": "raw"}
            """;
        Console.WriteLine(raw);
    }
}
Output
Point { X = 3, Y = 4, Length = 5 } Point { X = 3, Y = 0, Length = 3 } 5 True
Money { Amount = 12.50, Currency = EUR }
9.14
null
negative int
int 7
origin
point 1,2
long string abcdefg
array ending in 3
something else
1 two
2,3,4
DEFAULT
{"name": "raw"}

Records get the compiler-generated ToString, value equality and with copies. decimal keeps its scale (12.50), as it does on .NET.

Members, generics, iterators and async

Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class Counter
{
    public event EventHandler<int>? Changed;
    private int _value;
    public int Value
    {
        get => _value;
        set { _value = value; Changed?.Invoke(this, value); }
    }
}

public struct Vec(double x, double y)
{
    public double X = x, Y = y;
    public static Vec operator +(Vec a, Vec b) => new(a.X + b.X, a.Y + b.Y);
    public override string ToString() => $"({X}, {Y})";
}

public class Matrix
{
    private readonly double[,] _cells = new double[2, 2];
    public double this[int r, int c] { get => _cells[r, c]; set => _cells[r, c] = value; }
}

public static class StringExtensions
{
    public static string Shout(this string s) => s.ToUpper() + "!";
}

public class Program
{
    static IEnumerable<int> Fib()
    {
        int a = 0, b = 1;
        while (true) { yield return a; (a, b) = (b, a + b); }
    }

    static T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b;

    static async Task<int> SlowAdd(int a, int b)
    {
        await Task.Delay(10);
        return a + b;
    }

    public static async Task Main()
    {
        Console.WriteLine(string.Join(" ", Fib().Take(10)));
        Console.WriteLine(Max("apple", "pear") + " " + Max(3, 9));

        var c = new Counter();
        c.Changed += (_, v) => Console.WriteLine($"changed to {v}");
        c.Value = 42;

        var v = new Vec(1, 2);
        var w = v;          // a struct assignment copies the value
        w.X = 100;
        Console.WriteLine($"{v} {w} {v + w}");

        var m = new Matrix();
        m[1, 1] = 5;
        Console.WriteLine(m[1, 1]);

        Console.WriteLine("hello".Shout());

        var query = from n in Enumerable.Range(1, 10)
                    where n % 2 == 0
                    let sq = n * n
                    orderby sq descending
                    select new { n, sq };
        Console.WriteLine(string.Join(", ", query.Select(x => $"{x.n}:{x.sq}")));

        int Local(int x) => x * 2;
        Console.WriteLine(Local(21));

        var results = await Task.WhenAll(SlowAdd(1, 2), SlowAdd(3, 4));
        Console.WriteLine(string.Join(",", results));

        try { throw new InvalidOperationException("boom"); }
        catch (InvalidOperationException ex) when (ex.Message == "boom") { Console.WriteLine("filtered: " + ex.Message); }
        finally { Console.WriteLine("finally"); }

        long big = long.MaxValue;
        decimal price = 0.1m + 0.2m;
        Console.WriteLine($"{big} {price} {0.1 + 0.2}");
    }
}
Output
0 1 1 2 3 5 8 13 21 34
pear 9
changed to 42
(1, 2) (100, 2) (101, 4)
5
HELLO!
10:100, 8:64, 6:36, 4:16, 2:4
42
3,7
filtered: boom
finally
9223372036854775807 0.3 0.30000000000000004

Semantics

Numeric types

JavaScript has one number type, a 64-bit double. Transpose keeps the C# results anyway:

  • int, uint, short, byte and the other integer types up to 32 bits wrap on overflow and truncate on division, as in unchecked C#: int.MaxValue + 1 and int.MaxValue++ are both -2147483648, and 7 / 2 is 3.
  • long and ulong are runtime objects that hold all 64 bits, so long.MaxValue prints exactly. The exception is a long that comes from JavaScript through a binding, such as Blob.size in Transpose.Core. It stays a plain JavaScript number, so values above 2^53 lose precision.
  • decimal is a runtime object with .NET's decimal arithmetic: 0.1m + 0.2m is 0.3.
  • float and double are JavaScript numbers.

checked arithmetic is not supported, because JavaScript arithmetic does not report overflow. See Unsupported Features.

Structs

A struct declared in your project keeps value semantics. Assignment, passing by value, returning, boxing, adding it to a collection and with all copy the value, including nested struct fields. Structs from referenced libraries, BCL structs such as DateTime, and ValueTuple are copied by reference instead. Widening the copy to those would make every DateTime and tuple assignment allocate.

ref and out parameters

ref, out, in and ref readonly parameters work, including out var and discards:

static void Swap(ref int a, ref int b) => (a, b) = (b, a);
static bool TryHalf(int x, out int half) { half = x / 2; return x % 2 == 0; }

int a = 1, b = 2;
Swap(ref a, ref b);                                  // a = 2, b = 1
if (TryHalf(10, out var h)) Console.WriteLine(h);    // 5

A delegate whose signature has ref or out parameters works the same way when the target is a method. A lambda cannot take ref, out or in parameters; see Unsupported Features.

ref locals and ref returns

A ref local refers to the location itself, so a write through it changes the original. Ref-returning methods, local functions, properties and indexers work, including assignment to the result and ref readonly:

public class Buffer
{
    private readonly int[] _items = { 1, 2, 3 };
    public ref int this[int i] => ref _items[i];
    public ref int Find(int value) { /* … */ return ref _items[0]; }
}

int[] values = { 1, 2, 3 };
ref int first = ref values[0];
first = 10;                        // values[0] is 10

var buffer = new Buffer();
buffer[1] = 20;                    // assigns through the ref-returning indexer
buffer.Find(3) = 30;               // assigns through the ref-returning method
ref int slot = ref buffer[2];
slot++;

A ref-returning member of the Transpose runtime packages, such as Span<T>'s indexer, returns the element's value instead of a reference.

Positional patterns

A positional pattern on a record or a tuple reads its members. On any other type it calls the Deconstruct method C# binds to, an instance or an extension method:

public class Foo
{
    public int A; public string B;
    public void Deconstruct(out int a, out string b) { a = A; b = B; }
}

public static class VersionExtensions
{
    public static void Deconstruct(this Version v, out int major, out int minor)
        => (major, minor) = (v.Major, v.Minor);
}

var label = o switch
{
    Foo(1, "a") => "one",
    Foo(var a, _) when a > 5 => "big",
    _ => "other",
};
Console.WriteLine(new Version(2024, 5) is (2024, 5));   // True

goto

goto label is supported. A method that contains labels is compiled to a state machine, so a jump works forwards, backwards and across an await. goto case and goto default inside a switch are not supported.

dynamic

dynamic member access and invocation compile to plain JavaScript member access, so d.Name and d.Run() work. There is no runtime overload resolver: passing a dynamic to a method with a single candidate works, but a call that needs an overload chosen at runtime, such as Enumerable.Sum(d), emits a call to a function that does not exist.

Spans

Span<T> and ReadOnlySpan<T> are views onto an array: an array, an offset and a length. Writing through a span writes to the array it was made from. Supported:

  • the conversions from an array, a string (to ReadOnlySpan<char>) and a Span<T> to a ReadOnlySpan<T>, including a collection expression or a params argument list whose target is a span;
  • indexing, ^1, ranges (span[1..^1]), Slice, CopyTo, TryCopyTo, Fill, Clear, ToArray, ==, foreach and foreach (ref var x in span), ref span[i];
  • stackalloc into a span, and UTF-8 string literals ("abc"u8);
  • the MemoryExtensions searches (IndexOf, LastIndexOf, Contains, StartsWith, EndsWith, SequenceEqual) and, for ReadOnlySpan<char>, Trim, TrimStart, TrimEnd, IsWhiteSpace, Equals with a StringComparison and new string(span);
  • a string constant in a pattern or a switch over a ReadOnlySpan<char>, and list patterns over a span.
static int Sum(params ReadOnlySpan<int> xs) { var t = 0; foreach (var x in xs) t += x; return t; }

int[] values = { 1, 2, 3, 4 };
Span<int> tail = values.AsSpan(2);
tail.Fill(0);                              // values is 1, 2, 0, 0
foreach (ref var x in values.AsSpan()) x *= 10;

Span<int> buffer = stackalloc int[4];
ReadOnlySpan<byte> utf8 = "héllo"u8;       // 6 bytes
Console.WriteLine(Sum(1, 2, 3));           // 6
Console.WriteLine("  yes ".AsSpan().Trim() is "yes");   // True

stackalloc allocates an ordinary array, since JavaScript has no stack memory; a stackalloc into a pointer is unsafe code and rejected. MemoryExtensions.Reverse and Sort are not provided: C# would pick them over LINQ's Reverse() for every array.

Threading and cancellation

JavaScript runs your code on one thread. Task, Task.Run, Task.WhenAll, Task.Delay, TaskCompletionSource, CancellationToken and CancellationTokenSource all work on the event loop, and a lock statement on an object compiles to its body. System.Threading.Thread, Monitor, Interlocked, Lock and the wait handles are rejected at compile time. See Async Support.

© 2026 Curiosity. All rights reserved.