Classes & Structs
✅
Transpiled to ES6 Classes. A plain class has a module of its own whatever it declares, an empty one over an interface's defaults or a base included, and every module that names it imports it; a class whose chain of bases reaches an attribute or a [ServerOnly] type has none, three levels down as much as one (class Underline : Mark over class Mark : Attribute), a base from a referenced library included. An exception class of the app's is a class, with a module of its own (see try / catch / finally). The chain is read by what each base derives from and never by its name, so a class over an interface named like an attribute keeps its module, and a name is a module wherever any declaration of it is one. A class, a static class, a record or a struct declared inside another type is a module of its own too, named by the types that contain it and its own name joined by $ (Cart$Item, A$B$C), which no top-level type can take: every reference to it, inside its owner or out, names that twin, so a nested class beside a top-level class of its name is never taken for it, and one inside a server-only class or an attribute has none, as its owner has none: client code that names it is refused (EQ2010), and a page declared in it has no route, while one inside an exception class crosses as that class does (Since 0.2.0-preview.61). A record and a struct have a twin whatever they declare, unless marked [ServerOnly], a record over a server-only record included (Since 0.2.0-preview.61).
Interfaces
✅
Used for TypeScript type checking (erased at runtime). A default member (a property, an indexer or a method with a body; an indexer since 0.2.0-preview.61) reaches the twin of every class that takes it without declaring it: converted from the interface's source, or, for the SDK's own interfaces an app implements (a theme, a code language), delegated to the runtime's copy. An interface from any other compiled assembly has neither, and the build refuses the class (EQ1008) (Since 0.2.0-preview.59).
Enums
✅
Cross as their camelCase name (MainAlign.Start → 'start'), and a [Flags] enum as its number. .ToString(), interpolation and concatenation give the C# member name ("Start"), the same text the server prints, a flags combination's set flags ("Read, Write"), a value no member names as its digits, and nothing for a null nullable enum. ToString("D"), "X", "F" and "G", an interpolation's format and its alignment write what .NET writes, Enum.Parse, TryParse, GetName, GetNames, GetValues and IsDefined answer as .NET's, and a dictionary keyed by an enum crosses both ways, a name no member has being refused. Since 0.2.0-preview.15; the formats, the flags text, the statics of Enum and enum keys since 0.2.0-preview.60.
Generics
✅
Fully supported (erased at runtime).
Extension Methods
✅
Resolved via Semantic Model and transpiled to direct calls.
Async / Await
✅
Maps to async / await and Promise.
Lambda Expressions
✅
Maps to Arrow Functions () => {}.
Pattern Matching
✅
is patterns, Property Patterns, Recursive Patterns. A type with nothing bound (o is int or long, int => …, case int:) tests the type, a long as a BigInt, an integer as a whole number and a char as a string of one code unit. A constant in a pattern, x is Limits.Max included, is tested by its value: a null matches any absence, a decimal and a NaN compare by value, and a long constant is a BigInt, as every long is (Since 0.2.0-preview.60). A positional pattern over a type whose Deconstruct the app wrote, an extension's included, calls it once for each value a test deconstructs, across the arms of a switch and the alternatives of an or too, as .NET does, and reads the parts it hands back, as a deconstruction does: it read the members the outs are named after, which a part the Deconstruct computes is not (Since 0.2.0-preview.61).
String Interpolation
✅
Maps to Template Literals ${var} . A format with quoted text ({date:dd 'de' MMMM}) keeps its quotes, and a raw interpolated string keeps its braces ($$$"""{{x}}""" is {{x}}) (Since 0.2.0-preview.60). An alignment is the number it is, a constant by its name or an expression of constants included ({name,Widths.Name}, {name,-(Widths.Name + 1)}), in a StringBuilder too (Since 0.2.0-preview.62).
String & char literals
✅
A string or a char keeps the value C# reads, whatever spells it: a lone surrogate ("x\uD83D"), a control character, and the escapes only C# has ('\a', '\e', '\x041', '\U00000041'). A UTF-8 literal ("ab"u8) has no twin and fails the build (EQ1004) (Since 0.2.0-preview.60).
Null-Coalescing
✅
?? and ??= mapped to JS equivalents.
Object Initializers
✅
new Obj { Prop = 1 } over a record, a struct, a plain class, an exception or a vocabulary type whose twin the runtime transpiles (the spreadsheet's and the forms' models, CellRef, FieldError, SheetController…), applied once the constructor has returned, as C# applies it (a plain class took it as a trailing config object, an exception dropped it, and a transpiled vocabulary type handed it to a slot its twin does not have, before 0.2.0-preview.61; a component takes it as its props): every initializer of the type has run, the member's own included, and the values are read after them. A nested collection initializer (Items = { 1, 2 }) adds to the collection the member holds, through the Add the bound tree binds (an extension method's through its home, and a list's, a set's or an ICollection<T> member's as a call to it is); a nested object initializer (Inner = { N = 3 }) assigns into the object the member holds; and an entry ([key] = value) is written through the type's indexer. A dictionary's pair (Map = { { "a", 1 } }) is its Add, which refuses a key already there. Every part is evaluated in the method it was written in, so an await in an element works, and each element is applied before the next one's parts are evaluated, the member it adds to or assigns into read before its own parts, as C# applies it. An element with no lowering fails the build (EQ1004) (Since 0.2.0-preview.61).
field (C# 14)
✅
Guarded properties: the twin gets a $name slot (a name no C# field can take), a getter and the accessor's own body. A static one keeps its slot on the type, a record's included, and the slot starts as the property's initializer or its type's default (Since 0.2.0-preview.60). A record's and a struct's instance one runs its accessors' bodies over the slot, as a class's does, and one whose getter has a body is state its record compares and copies; it was held under the property's name, with its setter's body dropped, before 0.2.0-preview.61 (Since 0.2.0-preview.61).
Virtual and override properties
✅
A property a derived type can override or that overrides one (an auto-property declared virtual or override) keeps its value in a store of its own ($name) under accessors on the twin's prototype, in a class, a record and a struct alike, so every read reaches the override and no initializer reaches the accessor of another level: an auto-property over a computed one, or a computed one over an auto-property, threw at new or answered the base's value before 0.2.0-preview.61. An override that declares only the getter of a property whose base has both keeps the base's setter, as C# inherits it. A record compares, hashes and copies the store, as .NET compares a backing field. A twin with a store writes it in JSON under its property's name, so a server action receives the property by its C# name. One difference stays: in a type that overrides an auto-property with another, base.Name reads the override's value, where .NET reads the base's own field, since the two share one store (Since 0.2.0-preview.61).
Members that hide (new)
✅
A method that hides an inherited member, with new or with the same signature and no override, is reached as C# reaches it: through the base type the base's method answers, and through the hiding type the hiding one, a call, a method group, a base call and ?. alike, a generic method over its own type parameter included, and so does a member the runtime reads by its .NET name, so ((object)c).GetHashCode() is object's over a public new int GetHashCode(). Its twin holds it under a name of its own (name$1), and an override of a new virtual method fills that method's place. So does a method beside an explicit interface implementation of its name, so a call through the class reaches it and one through the interface the explicit one. One that hides a member, or stands beside such an implementation, and answers an interface fails the build (EQ1007), and a property or a field that hides one still shares its name with it (#563, since 0.2.0-preview.61). init accessors
✅
Emitted as a JS setter, because an init body is where a type states its invariant, and dropping it would lose the check on the client.
Collection Initializers
✅
new List<int> { 1, 2 } maps to [1, 2]. A list built with an argument and an initializer holds both, as C# builds it: a copy of the source, never the source itself, or nothing for a capacity, then each element in order, the source or the capacity read first. A capacity is still evaluated, and a negative one is refused as the constructor refuses it. So new List<int>(source) { 3 } is [...source, 3] and new List<int>(10) { 1, 2 } is [1, 2], written out or target-typed, and List<int> c = new(source) is a copy. A capacity dropped the elements, and a source was joined to them with a comma, before 0.2.0-preview.61 (Since 0.2.0-preview.61).
IEnumerable<T> of your own
✅
A class, a record or a struct that implements IEnumerable<T>, or only IEnumerable, is enumerated as its own GetEnumerator() says, by foreach, a spread, string.Join and every LINQ operator, each through the one C# binds for it: a foreach over the class walks its own public GetEnumerator() (a struct enumerator's included), and LINQ, a spread, string.Join and a foreach over the interface walk the IEnumerable<T> one, explicit or not, so the two may walk different sequences, as in .NET. It is the sequence an iterator method yields, or an enumerator of your own, a class that is its own enumerator included, walked by its MoveNext and Current and disposed when the walk ends, a break included. A derived class's override answers, the explicit IEnumerable.GetEnumerator() beside the generic one is the same sequence, and so is an explicit Current beside a public one. A class with only a public GetEnumerator() is walked by foreach too. The twin has one iteration and one Current, so a type that implements IEnumerable<T>, or IEnumerator<T>, over two types fails the build (EQ1007), and so does an explicit IEnumerable.GetEnumerator() or Current that answers otherwise than the member the twin keeps, where one that hands back its answer (=> GetEnumerator();, => Current;) is held as one. A .NET collection's own GetEnumerator() (=> items.GetEnumerator()) has no translation and fails the build (EQ2004): yield the items instead (#612, since 0.2.0-preview.61). Deconstruction
✅
var (a, b) = tuple maps to [a, b] = tuple, and so do (var c, var d) = … and nested designations. A record or a struct deconstructs through the Deconstruct it calls, in every shape: a declaration, an assignment ((a, b) = point, (var a, b) = point), a foreach (var (a, b) in points) and a nested deconstruction. A record's own reads its members, and a Deconstruct the app wrote runs. The targets' receivers and indices are evaluated before the value, as C# evaluates them, so (xs[i++], xs[i++]) = point writes the elements C# writes (Since 0.2.0-preview.60). Each part reaches its target as C# puts it there: converted to the target's type (an int part into a long), and written by what the target is, a dictionary's entry through its class, in an assignment, a declaration and a loop (Since 0.2.0-preview.60).
Expression variables
✅
What a pattern, an out var or a deconstruction declares is declared where C# declares it, in every kind of member: in the enclosing block after an if or an expression statement, inside a loop or a using, and fresh on every loop iteration and every call, so a closure keeps its own value. A name JavaScript reserves (@class) is renamed once, at its declaration and every reference (Since 0.2.0-preview.59).
Local Functions
✅
Transpiled to inner functions. An out or ref parameter carries its value back to the caller, as a method's does (Since 0.2.0-preview.60).
for loops
✅
1:1. A loop's variable is one per loop, as in C#: a closure made in the body reads its current value, never a copy of its own iteration's, and so is what its initializer declares (out var n, var (i, j) = (0, 3)), a head of expressions included (for (Seed(out var n); …)) (Since 0.2.0-preview.60).
Delegates
✅
Called as values, by name, through a member, or from any expression (handlers[0](x), Make()(x), since 0.2.0-preview.60).
Records (with)
✅
A copy of the record onto its own class, then the members the expression names: like .NET's copy, it runs no initializer and no constructor. A struct's with, and one on the code engine's records, go through the twin's own copy. A record whose chain declares a copy constructor is copied through it, as C#'s with is: a declared one starts its members at their zero, runs its base's copy with what : base(…) passes and then its body, and a synthesized level copies its own members (Since 0.2.0-preview.61).
Constructors
✅
A record's, a struct's or a plain class's twin constructor is the C# constructor the call binds, each one reached by the counts of arguments it takes, its optional parameters counted (a plain class kept its widest one before 0.2.0-preview.61). One that does its own work (the primary one, or an explicit one that does not chain) binds its parameters, runs every initializer in declaration order, a field's and a property's alike, a derived type's before its base's constructor, calls its base's constructor with its own arguments and runs its body; one that chains with : this(…) runs the one it chains to and then its own body, even when that one's body returns early. Each argument lands in its parameter's place and is evaluated in the order it is written, in new, : this(…), : base(…) and a base clause alike: a named one, an omitted optional one, and a params array passed whole (named or not) or packed from its elements; a variable an argument declares (out var n) is declared where it is evaluated, and a constructor's parameter is a variable its body may assign. A record's copy constructor is no branch. Two constructors that take the same count of arguments fail the build (EQ1009), and so do a chain to a constructor that chains in turn and : this() to a struct's implicit constructor beside constructors of its own. A struct's zero (default, an array's slot, new S() with no constructor of its own, a generic struct's, which holds each type argument's zero, and one from another assembly included) runs none of it, its static constructor included. A plain class's primary constructor binds its parameters as a record's does, and one a member reads is held on each instance under its name, so one that lands on another member's name fails the build (EQ1007). One difference stays: a base constructor that reads a derived class's member through a virtual member reads it unset, where .NET reads it initialized, since JavaScript lets nothing touch the instance before super() returns (Since 0.2.0-preview.61).
Indexers
✅
An instance indexer is its twin's item(…) and setItem(…, value), and every access to it calls them: a read, a write, a compound assignment, ++ on a value of any type, ??=, ?[…], a deconstruction's target, a key from the end (^1, at the count its type names) and an object initializer's entry, the receiver and the keys evaluated once each, each key in its parameter's place (a named key, an omitted optional key, a params key). An assignment answers the value it assigned, whatever the setter does with its copy, and an explicit implementation of an interface's indexer answers the interface's accesses. A second indexer in a type, or any member on Item or SetItem beside one, fails the build (EQ1007) (Since 0.2.0-preview.61). An access through a list's face (IList<T>, IReadOnlyList<T>, IList) reaches whatever the face holds when it runs, an array's or a list's element and a twin's item and setItem alike, in every one of those forms, and its Count counts a type of your own by its own Count, never by a Length beside it: the face read a subscript and a length, which a twin does not answer, before 0.2.0-preview.61 (Since 0.2.0-preview.61).
Ranges
✅
x[a..b] over a string, an array or a list is JavaScript's slice, and an endpoint that may be ^0 resolves against the length as Index.GetOffset does. Over a type of your own with a Length (or a Count) and a Slice(int start, int length), it calls that Slice as C# lowers the range: the receiver once, then the endpoints in the order they are written, then the count, read only where an endpoint counts from the end or the end is left open. So strip[1..3] is Slice(1, 2), where it called the twin's slice(1, 3) before 0.2.0-preview.61. A range handed to an indexer that takes the Range itself fails the build (EQ2004), as a Range stored as a value does (Since 0.2.0-preview.61).
Static initialization
✅
A type's statics start at their zero, then the initializers run in declaration order, then the static constructor, once, the first time one of them is read or written: static int A = B + 1; static int B = 2; makes A 1, as in .NET, in a record, a struct, a class, a static class and a component. A type with a static constructor runs it before its first instance and the first use of any of its static members, a method, a computed property and an operator included. The initializers run in declaration order across fields, properties and field-like events, and the static constructor's body after them, a return in it ending it alone; one that throws makes every use of the type throw a TypeInitializationException, as .NET does; a static whose initializer C# folds to a constant is its value; and a component's static constructor runs once (Since 0.2.0-preview.61).
typeof
✅
Maps to type name string literal.
Base types
✅
A class extends its base's twin however the base is written: bare, with its namespace, through an alias or with global:: (Since 0.2.0-preview.60).
Qualified type names
✅
A type reached through its namespace, part of it, the whole of it or global:: is imported as a type reached through a using is: inside App.Chat, Portal.Fold.Text(n) answers what Fold.Text(n) answers under using App.Portal; (Since 0.2.0-preview.61).
base calls
✅
Maps to super keyword.
using static
✅
A member reached by its simple name answers what its qualified spelling answers: PI is Math.PI, Join(…) is string.Join(…), an enum's High is Level.High, and Now under using static System.DateTime is DateTime.Now (Since 0.2.0-preview.61; until then a .NET member outside Math, MathF, string, a constant and an enum failed the build even where its qualified spelling translated). A .NET member whose qualified spelling has no translation either fails the build (EQ2004) instead of reaching the browser (Since 0.2.0-preview.60).
A type's own Count
✅
A type the app or a library it references declares reads its own Count: a library's domain model read it as an array's length, undefined (Since 0.2.0-preview.60).
cast & as
✅
Maps to JS passthrough/truncation.
sizeof
✅
Maps to C# primitive sizes.
Anonymous Methods
✅
delegate(...) { ... } maps to arrow functions.
params
✅
A REST parameter (...xs). Both C# call forms work: expanded arguments and an array passed whole, which spreads. Since 0.2.0-preview.15.
stackalloc
✅
Maps to Typed Arrays (e.g., Int32Array).
yield return
✅
Maps to JS Generator Functions (function*).
try / catch / finally
✅
The catch clauses are ONE JavaScript catch that tries them in C#'s order: each by its exception type, a type derived from it included, and by its when filter, which declares its own variables (out var n, a pattern) and answers false where it throws, as .NET's does. An exception no clause takes goes on, and throw; rethrows the exception caught. An exception is a JavaScript Error that carries its .NET types, an exception class of the app's own and Exception e = new("…") included, so e is ArgumentException, a switch over exceptions and as test the type too, and an exception the runtime throws is the type .NET throws there (an overflow, a missing key, a bad format, a refused cast); a JavaScript TypeError is a NullReferenceException. One difference stays, the platform's: .NET runs a filter BEFORE the finally blocks between the throw and the clause, and JavaScript after them. Since 0.2.0-preview.60. A framework exception's message is composed as .NET composes it: where no message or a null one was given, the text .NET writes for that constructor, read from .NET itself, then (Parameter 'x'), and the actual value and a disposed object's name on lines of their own, an aggregate's inner messages after its own and a type initializer's sentence, and ParamName, ActualValue, InnerException and TypeName read what the constructor took, an aggregate's InnerException being its first inner one, the runtime's own argument exceptions naming their parameter too (Since 0.2.0-preview.61). A null argument is refused as .NET refuses it, an ArgumentNullException naming the parameter, by LINQ's Max, Min and ToDictionary, by a LINQ operator handed a sequence that is not a list (Concat's second, Zip's first), by new Guid(text), char.GetUnicodeCategory(text, i), CancellationToken.Register and CreateLinkedTokenSource, and CompareTo(object) answers 1 for a null, as .NET's does. A member lowered to JavaScript's own method, LINQ over a list among them, still reads a null through null, a NullReferenceException: the conformance suite calls every member of the translated surface with a null for each reference parameter, and lists those in null-argument-gaps.baseline.txt with their reasons (Since 0.2.0-preview.61). An exception class of the app's is a class over the runtime's exception base, the Error that carries its types: its fields, its properties, its constructors and its methods are its own, and its base call hands what a new of its .NET base hands, so : base(message, inner) reaches Message and InnerException, : base("bad", name) over ArgumentException reads bad (Parameter 'x') and its ParamName, a class over InvalidOperationException that gives no message reads that type's own text, a missing message over Exception is .NET's default (Exception of type 'App.Failure' was thrown.), a member of its own under a name the base holds (a Name, an override of Message or of ParamName) answers for it, and a generic one is caught by its construction (catch (Failed<int>)) from the moment its constructor runs, a throw this there included. A missing message names the type as .NET names it at run time (App.Failed1[System.Int32], App.Outer+Inner). It is transpiled as any class is: one the browser never sees, whose members reach what it cannot run, is marked [ServerOnly], and two of its constructors that take as many arguments are refused (EQ1009), save the one only .NET's serialization calls, (SerializationInfo, StreamingContext), which Visual Studio's template writes and the twin leaves out, and a call of that one the browser would make, a new or the base call of another constructor, fails the build (EQ1009). An exception built or tested where a type argument it carries is a type parameter fails the build too (EQ2014), since JavaScript erases it: new Failed<T>() in a generic method, catch (Failed<T>), new Missing() inside the generic class that declares it. One declared inside another type is a class too, its twin named by its owner (Holder$Nested), and so is what an exception class declares, a record of the reason it carries (Rejected$Reason). In a snippet compiled on its own, with no project to read its bases from, an exception class of the app's is still the runtime's Error` with its types and none of its members (#611, since 0.2.0-preview.61). throw expressions
✅
x ?? throw …, c ? a : throw … and => throw …, the exception evaluated only where it is thrown, an await in it included. Since 0.2.0-preview.60.
lock
⚠️
Transpiled to the body alone (JS is single-threaded). Its expression runs once, in front of it, unless it only reads a variable, a field or this (Since 0.2.0-preview.60; one that declares a variable, since 0.2.0-preview.59). new object(), the usual gate, is a fresh object, with or without an empty initializer.