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
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);
}
}
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
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}");
}
}
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,byteand the other integer types up to 32 bits wrap on overflow and truncate on division, as in unchecked C#:int.MaxValue + 1andint.MaxValue++are both-2147483648, and7 / 2is3.longandulongare runtime objects that hold all 64 bits, solong.MaxValueprints exactly. The exception is alongthat comes from JavaScript through a binding, such asBlob.sizeinTranspose.Core. It stays a plain JavaScript number, so values above 2^53 lose precision.decimalis a runtime object with .NET's decimal arithmetic:0.1m + 0.2mis0.3.floatanddoubleare 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 aSpan<T>to aReadOnlySpan<T>, including a collection expression or aparamsargument list whose target is a span; - indexing,
^1, ranges (span[1..^1]),Slice,CopyTo,TryCopyTo,Fill,Clear,ToArray,==,foreachandforeach (ref var x in span),ref span[i]; stackallocinto a span, and UTF-8 string literals ("abc"u8);- the
MemoryExtensionssearches (IndexOf,LastIndexOf,Contains,StartsWith,EndsWith,SequenceEqual) and, forReadOnlySpan<char>,Trim,TrimStart,TrimEnd,IsWhiteSpace,Equalswith aStringComparisonandnew string(span); - a string constant in a pattern or a
switchover aReadOnlySpan<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.
Related
- Unsupported Features — what is rejected, and the error for each case
- LINQ Support
- Async Support
- Reflection