Arithmetic / bitwise / comparison
✅
integer division truncates; %, shifts, & | ^ ~ native for an int; a uint's & | ^ >> ~ stay unsigned, and a long's shifts mask their count to six bits and drop the bits past 64, as .NET's do (Since 0.2.0-preview.58); checked(expr) throws OverflowException, unchecked(expr) wraps to 32 bits (\| 0/>>> 0), a negation included, and an await inside either is the method's own (Since 0.2.0-preview.60). Default-context int overflow does not wrap (JS float64).
Math.* / MathF.*
✅
the same table as double.* / float.*, with .NET's own compositions; Round is .NET's (exact midpoint, every mode)
decimal
✅
exact via Decimal (literals + + - * / % == != < > <= >=, the remainder with the dividend's sign, Since 0.2.0-preview.58 for %); the constants MaxValue, MinValue, Zero, One and MinusOne (Since 0.2.0-preview.60); decimal.Parse/TryParse by .NET's grammar and rounding, the NumberStyles a call names included; Convert.ToDecimal and the casts from double/float at .NET's 15 and 7 digits
Numeric constants
✅
int.MaxValue, double.Epsilon, … → literals
Parsing / Convert.*
✅
Parse/TryParse on every numeric type, and Convert.ToInt32/ToInt64/ToDouble/… of text, by .NET's grammar (Since 0.2.0-preview.58); bool.Parse/TryParse and Convert.ToBoolean of text as .NET's Boolean.TryParse reads it: "True" or "False" in any case, trimmed of white space and NULs, and .NET's exceptions, and Convert.ToBoolean of a number as whether it is not zero, of a bool as itself, and of a char or a date as .NET's InvalidCastException (Since 0.2.0-preview.59); Convert.ToString/…; Convert.ToDateTime of a text as DateTime.Parse reads it in the culture the call names, a null one as MinValue (Since 0.2.0-preview.62, where it answered the text); Convert.ToBase64String, ToBase64CharArray, ToHexString and ToHexStringLower write .NET's encoding of a byte[], its offset, its length and its line breaks every 76 characters included, and refuse what .NET refuses in its words; a span that is not a collection expression fails the build (EQ1004) (Since 0.2.0-preview.62, where each wrote the array's 1,2,255)
Strings
✅
Substring/IndexOf/Replace/Split/Pad/Trim/Trim(char)/Concat/Format/Join/IsNullOrEmpty/IsNullOrWhiteSpace; Trim(chars) and its two halves read the string and then the characters once, an await among them included, and no characters (an empty or a null array) trims white space, as .NET does (Since 0.2.0-preview.60); Trim(), its two halves, IsNullOrWhiteSpace and a bare Split() read .NET's white space, where JavaScript's leaves U+0085 and takes U+FEFF (Since 0.2.0-preview.58); a string's own Equals, StartsWith, EndsWith, Contains, IndexOf and LastIndexOf (with a start and a count) and Replace given a StringComparison answer by that comparison, written out or held in a variable: OrdinalIgnoreCase keeps .NET's casing (the Kelvin sign, the long s and the dotless i are not k, s and I, and a surrogate pair is read as its code point), a start or a count outside the string throws .NET's exception, Replace writes its replacement as text, and CompareTo orders by the current culture, as string.Compare does (Since 0.2.0-preview.60). IndexOf and LastIndexOf of a string from a start, and for a count, with no comparison stop at their count, read LastIndexOf's start as the last char a match may cover, and throw .NET's exception for a range outside the string or a null value (Since 0.2.0-preview.62, where the count was dropped and LastIndexOf("bc", 4) answered the match beginning at 4). IndexOf(char, startIndex[, count]) and LastIndexOf(char, startIndex[, count]) search that range and throw .NET's exception for a start or a count outside the string: LastIndexOf's start stands on a char of the string, and an empty string answers -1 (Since 0.2.0-preview.61, where the start was clamped and the count dropped). A SEARCH by a culture comparison, or with a CultureInfo, fails the build (EQ1004), because the browser has no culture-aware search: search by Ordinal or OrdinalIgnoreCase, or compare whole strings with Equals or string.Compare. The overloads without a comparison search ordinally in the browser and by the current culture in .NET (#532). char.*
✅
ToUpper/ToLower/IsDigit/IsLetter/IsWhiteSpace/… (Unicode-aware; IsWhiteSpace is .NET's set, U+0085 in and U+FEFF out, Since 0.2.0-preview.58; ToUpper, ToLower and their invariant pair case a char as a string of one, in the culture the call names, and a null culture throws .NET's ArgumentNullException, Since 0.2.0-preview.62)
StringBuilder
✅
compat type: Append(incl. bool→"True"/"False")/AppendLine("\n")/Insert/Remove/Replace/Clear/Length/ToString. The overloads that count or take a range write what .NET writes, Append('x', 3), Append(text, start, count), Insert(i, text, count), Replace over a range and ToString(start, length), a char[] as its characters and a null as nothing, and each refusal is .NET's (Since 0.2.0-preview.61). AppendFormat and AppendJoin append what string.Format and string.Join write, Capacity follows the chunks .NET allocates through every edit, MaxCapacity and EnsureCapacity answer, the Chars indexer reads and writes a char, Length cuts the text or fills it with \0, Equals(StringBuilder) compares the text, Equals(object) the reference (since 0.2.0-preview.64), and CopyTo copies it, each refusing what .NET refuses; an interpolated Append($"…") or AppendLine($"…") appends each part in turn, as .NET's handler does, so a hole that reads the builder sees the parts before it, a named argument is bound to its parameter, and a text longer than the browser's string can hold is .NET's OutOfMemoryException; a provider other than the current culture on Append, GetChunks and the span overloads fail the build (Since 0.2.0-preview.61)
CancellationTokenSource / CancellationToken
✅
the runtime's, as .NET's: a source (with a delay or none), its Token, IsCancellationRequested, Cancel, CancelAfter, Dispose and CreateLinkedTokenSource; a token's None (and default, and new CancellationToken(bool)), IsCancellationRequested, CanBeCanceled, ThrowIfCancellationRequested (an OperationCanceledException, "The operation was canceled.") and Register of a callback, whose registration Disposes and Unregisters. Callbacks run once, the last registered first, one registered late runs at once and is given back the default registration (its token is None), and the ones that throw are gathered into an AggregateException, as in .NET. A method of the three passed as a callback (token.Register(source.Cancel)) keeps its receiver, and a linked source, disposed, lets go of the tokens it follows. A disposed source lets go of its callbacks too, a registration still unregistering once after it, and new CancellationTokenSource(delay, timeProvider) fails the build with EQ2004: the runtime's source is timed by the real clock. A delay is taken up to 4294967294 ms, as .NET takes it, and a month is waited out though a browser's timer holds about 24.8 days. default(CancellationTokenRegistration) is the registration of nothing, whose token is None. Any other member of the three fails the build with EQ2004 (Since 0.2.0-preview.61)
LINQ
✅
Where/Select/SelectMany/Where-Select(indexed)/OrderBy/Distinct(By)/GroupBy/ToDictionary/ToLookup/Zip/Chunk/MinBy/MaxBy/Take(While)/Skip(While)/Aggregate/Sum/Min/Max/Average/Count/Any/All/First/Last/Concat/Reverse/Join/GroupJoin/ThenBy/ThenByDescending (Join/GroupJoin = order-preserving hash join over primitive keys; OrderBy+ThenBy = single stable composite sort, source copied). IGrouping from GroupBy/ToLookup is usable as a sequence (iterate, g.Select/g.Sum/g.Count()) and exposes g.Key; ILookup [key] indexer is not modelled. A comparer that asks for the default passes ToDictionary, ToLookup, GroupBy, Distinct and ToHashSet, and any other fails the build (EQ2007) (Since 0.2.0-preview.61).
Collections
✅
List, Dictionary, HashSet (incl. initializers and .Count; a set is the runtime's set, held by slot and finding its elements by value where its element type does, Since 0.2.0-preview.61); Queue/Stack/LinkedList compat; sorted family SortedSet/SortedDictionary/SortedList (key-sorted enumeration). Every dictionary is the runtime's dictionary class, $eq.collections.dictionary, enumerating by slot as .NET's does, a record/struct/tuple key found by value (construction and copy, d[k] get/set, ContainsKey/Add/Remove/Clear/TryGetValue/GetValueOrDefault/TryAdd/ContainsValue, Keys/Values/Count, foreach) (Since 0.2.0-preview.60). Reading a missing key throws KeyNotFoundException, and so do a compound assignment and an increment on one (Since 0.2.0-preview.58 for the increment). ILookup[key] indexer returns the group (or empty for an absent key). ICollection<T>'s Add and Clear reach the collection behind the interface, and IList<T> and IReadOnlyList<T> read and write a type of your own through its indexer and count it by its Count (Since 0.2.0-preview.61).
enum
✅
member-name string, a [Flags] enum its number (equality/switch/ternary, text and formats, the statics of Enum, Equals and GetHashCode by its underlying value, an alias as the member it aliases)
long/ulong
✅
exact via BigInt (long helper); literals 5L→5n, and constants too (TimeSpan.TicksPerSecond → 10000000n, Since 0.2.0-preview.60), wire as JSON string
DateTime
✅
tick-precise DateTime compat type: ctors, components, Add* to the tick as .NET 7+ lands on it (a fraction truncated, a count or a result out of range refused in .NET's words, AddMicroseconds included, Since 0.2.0-preview.59), -→TimeSpan, comparisons, .ToString()/format; ISO-8601 wire + hydration. ToString(), ToString(format) and ToString(format, provider) print as .NET prints them (no specifier, or a null or empty one, is the current culture's G, and no time zone moves the value's parts but U's): every one-letter standard specifier (d to Y, s, u and R from the value's own parts, o from them with the zone its kind names, U moved to UTC unless it is UTC already), a letter that is no standard specifier refused as .NET refuses it, a custom picture token by token with its fraction of a second (HH:mm:ss.fff), the culture's separators for / and : and the era for g (Since 0.2.0-preview.61), the invariant culture's patterns for CultureInfo.InvariantCulture or no culture in force, and a named culture refused at the build (EQ2108); ToShortDateString() and its siblings are d, D, t and T (Since 0.2.0-preview.60). Add, Subtract, AddMonths, AddYears and the + and - operators, compound forms included, refuse a result off the calendar in .NET's words, the operators naming their parameter t (Since 0.2.0-preview.60). Every constructor the compiler lets through builds the value .NET builds, each argument in its parameter's place, a named one included: the components with a millisecond, a microsecond and a kind, the ticks with a kind, and a DateOnly with a TimeOnly, a component, a kind or ticks out of range refused in .NET's words, and an overload that takes a Calendar refused at the build (EQ1004). A value carries its Kind (Now and Today local, UtcNow UTC, SpecifyKind), which arithmetic keeps and equality, ordering and the hash leave out; ToLocalTime() and ToUniversalTime() convert by it in the browser's time zone, Microsecond and Nanosecond answer, and the JSON writes a UTC value with Z and a local one with its offset and reads them back as System.Text.Json does (Z a UTC time, an offset moved to the browser's local time), while DateTime.Parse moves a written zone to the local time. A local time made from an instant keeps which of two repeated hours it is, as .NET keeps it beside the kind, through arithmetic until SpecifyKind, so its way back to UTC is that instant, and a date's text writes its kind: o and K end a UTC time with Z and a local one with its offset, and z, zz and zzz write the zone's offset for anything not UTC (Since 0.2.0-preview.61). Compare, Equals and UnixEpoch answer, and ToBinary, FromBinary, ToFileTime, FromFileTime and their UTC forms, ToOADate, FromOADate, IsDaylightSavingTime and GetTypeCode fail the build (EQ2004), where each named a member no twin had (Since 0.2.0-preview.64)
TimeSpan
✅
tick-precise TimeSpan compat type: From*, ctors, components/totals, + -, comparisons, .NET "c" .ToString(), and its standard and custom formats (Since 0.2.0-preview.62); Parse and TryParse read as .NET reads with no format, each part against its bound (Since 0.2.0-preview.62); "c" wire + hydration; Microseconds, Nanoseconds, TotalMicroseconds, TotalNanoseconds, Multiply, Divide, Subtract, Compare, Equals, *, / and a sign in front, a product or a quotient rounded to the even tick and a NaN or an overflow refused in .NET's words, and a sum, a difference, Negate and Duration past the range refused as .NET refuses them; TryFormat fails the build (EQ2004) (Since 0.2.0-preview.64); a factor of any type C# converts to a double, a long, a char and a nullable one included, which answers null when it is null, on either side, and *= and /= (Since 0.2.0-preview.64)
DateOnly / TimeOnly
✅
compat types (.NET 6+): ctors, components, Add*(+ TimeOnly wrap, one product converted as .NET 9+ converts a double, Since 0.2.0-preview.59, a DateOnly leaving the calendar refused in .NET's words, Since 0.2.0-preview.60), comparisons, ToString(), ToString(format) and ToString(format, provider) as .NET prints them in the culture in force: d and t with no format, the standard specifiers each type takes and a custom picture of its own parts, anything else .NET's FormatException, and ToShortDateString(), ToLongDateString(), ToShortTimeString() and ToLongTimeString() as d, D, t and T (Since 0.2.0-preview.61); ISO wire + hydration; a DateOnly's ToDateTime, and a TimeOnly's Add, IsBetween, ToTimeSpan, Microsecond and Nanosecond (Since 0.2.0-preview.64)
DateTimeOffset
✅
tick-precise compat type (wall-clock + offset, compared by the instant): ctors, components, MinValue/MaxValue (Since 0.2.0-preview.58), Offset/UtcDateTime/LocalDateTime, ToOffset, Add* to the tick with AddMilliseconds/AddMicroseconds, the clock time and then the UTC time refused in .NET's words (Since 0.2.0-preview.59), AddMonths and AddYears included (Since 0.2.0-preview.60), From/ToUnixTime* (ToUnixTimeSeconds and ToUnixTimeMilliseconds a long, rounded down before 1970 as .NET rounds it, Since 0.2.0-preview.61), - →TimeSpan, instant comparisons, ToString() and its overloads as .NET prints them in the culture in force: the short date, the long time and the offset with no format, every standard specifier but U, o and K writing the offset and u and R converting by it (Since 0.2.0-preview.61); ISO+offset wire + hydration. Every constructor builds the value .NET builds: the components with a millisecond and a microsecond, the ticks, a DateOnly with a TimeOnly, and a DateTime, which without an offset takes the browser's for a time that is not UTC; an offset that is not whole minutes, is past fourteen hours, or is not zero for a UTC DateTime is refused in .NET's words. Now is the instant at the browser's offset, LocalDateTime and ToLocalTime() read the browser's time zone, and Microsecond and Nanosecond answer (Since 0.2.0-preview.61). DayOfYear, TotalOffsetMinutes, ToUniversalTime(), EqualsExact, Compare, Equals and UnixEpoch answer, and ToFileTime fails the build (EQ2004) (Since 0.2.0-preview.64)
record / struct / value tuple
✅
Value semantics. Records/structs are instances of their twin classes (positional new Point(1,2) calls the constructor C# calls, and an object initializer is applied after it), tuples are arrays with element access by position (t.Item1) and by declared name ((int X, int Y).X) → index. ==/!=, .Equals, Distinct compare structurally via $eq.equals, and Contains, IndexOf and a set by the element type's default comparer, a tuple's array member by reference (Since 0.2.0-preview.61), and ==/!= over two nullable record structs compare by value too, two absent values equal and an absent one unequal to any value, as C# lifts them, where the browser compared two references (Since 0.2.0-preview.64), and so do .Equals, Nullable.Equals and EqualityComparer<T>.Default.Equals over a nullable record or struct, by its own Equals, never its ==, where the build refused them (Since 0.2.0-preview.64); with copies-and-replaces. A mutable struct and a value tuple are copied where C# copies them, so a write through one name never shows through another the value was assigned, passed, returned or boxed to: the twin copies on write, and copies this where it leaves its struct's member (Since 0.2.0-preview.61). Deconstruction var (a, b) = … works for tuples (array destructuring, discard holes) and records (object destructuring by Deconstruct order). Records emit as named JS classes carrying their user instance methods + a structural equals, prototype-preserving with, and .NET toString; value semantics are unchanged. A record's equals compares its runtime type (a Dog never equals an Animal), what its base compares and every field it declares, a private one included, and its text names the members .NET's PrintMembers writes, in its order: its base's members first, then its own, a computed property and a property whose getter has an accessibility of its own included, an override left to the base, each once, and Name { } for none. Two members of a record or a struct that land on one name of the twin (struct S(int x) { public int X … }, a field celsius beside Celsius) fail the build (EQ1007); a primary constructor's parameter read only by an initializer (struct Point(int x) { public int X { get; } = x; }) is the constructor's, and builds; an abstract property holds nothing. Each value is written as .NET writes it: a bool as True, an enum by its member's name, a null as nothing and a number in the culture in force, and a plain class or struct that writes no text of its own writes its type's full name (My.App.Outer+In), where the browser wrote [object Object] or a record's text; a generic one does not yet (Since 0.2.0-preview.61). A positional parameter whose property the record declares (record Box(int X) { public int X { get; set; } = X; }) is one member, the declared one (Since 0.2.0-preview.61). A member starts as its declaration says, whatever the expression: a positional parameter, a property's or a field's initializer (= 1.5m, = 5 on a long, = new(), = "#" + Id), written once, in the class's constructor (Since 0.2.0-preview.59), which runs every initializer on every construction, whatever an object initializer then sets (since 0.2.0-preview.61; see Constructors above). A record's and a struct's members are lowered as a class's are: an async method, an iterator, an out or ref parameter, and the variable an expression body's pattern binds (o is P p && p.X == X) work in their methods, operators, conversions and computed properties, their own and a default an interface supplies, and a getter that yields returns its sequence in a class too (Since 0.2.0-preview.60). The build pipeline discovers records by scanning and emits each as its own module; components that reference a record import it automatically (reactive, no hardcoded list). SSR values are re-hydrated back into the record class on the client (recursively, restoring nested records and compat members), so methods/instanceof survive. Covers positional and body records and plain structs, plus record inheritance (extends + super) and generic records, whose equality compares the closed type a value was built as: a Box<int> never equals a Box<double>, and one built inside generic code, whose type arguments the build cannot know, is not taken for another type (Since 0.2.0-preview.61). Record-keyed dictionaries (Dictionary<RecordKey, V>) find a key by value, so equal-by-value keys collide as in .NET. Each member compares as EqualityComparer<T>.Default compares its type, so an array, a list or a collection's interface (IReadOnlyList<int>) by reference, and a tuple's Equals compares its elements the same way, a tuple of another arity unequal and a null pair equal only to another; a tuple seen only as object, or as an interface a tuple implements, is an array on the web and compares by its elements (Since 0.2.0-preview.61). A value tuple's text is .NET's, (1, a): each element as its type writes it in the culture in force, a nested tuple and an anonymous object included, a null one as nothing, and eight elements or more flat, through ToString() and its method group, a hole (one with an alignment or a format too, which a tuple ignores), a concatenation, string.Concat, string.Join, string.Format, a StringBuilder, Convert.ToString and a record's member, Zip's pairs among them, where the browser wrote 1,2. An element that is an array, a list, a dictionary, a set or a delegate writes its type's name, System.Int32[], as .NET does, and so does the value itself wherever it is written into text, where the browser wrote 1,2 or a function's source. Its ToString method group writes the same, and an enum's group its member's name, where the group wrote JavaScript's text. Equals(object) finds a tuple of other element types unequal, ((byte)1, 2).Equals((object)(1, 2)) is false, where the browser saw the same array. The method group t.Equals compares as the call does, and a Nullable tuple's Equals is its value's (Since 0.2.0-preview.64). A tuple held as object, whose type the build cannot see, still prints 1,2 and compares by its elements, and so does one handed to the Equals method group's delegate.
Anonymous object (new { A = 1 })
✅
A plain object, its members under their twin names ({ a: 1 }). Equals, a call or a method group, compares as the Equals C# writes for an anonymous type: each member by its own type's equality (an array or a list by reference, a decimal and a tuple by value), and only another anonymous object of the same members in the same order can be equal, so other members, the same members in another order and a record with the same members are not. The call threw before. Members of other types make another type too: new { A = (byte)1 }.Equals(new { A = 1 }) is false. Through null, Equals, ToString and making the delegate of either method group throw NullReferenceException, as in .NET, while a hole and a concatenation write nothing. == stays reference equality, and GetHashCode hashes equal objects equal, and a set, Distinct and GroupBy find equal objects one key, Distinct keeping the first of each. Its text is .NET's, { R = 255, Name = a } and { }: the C# member names in their order, each member as its type writes it in the culture in force, through every route a tuple's text takes, where the browser wrote [object Object] (Since 0.2.0-preview.64). One held as object, or handed to the Equals method group's delegate, still prints [object Object] and compares by its members, whatever their types.
object members
✅
object.Equals(a, b) answers as .NET's: true for two nulls or one reference, false for one null, and otherwise the FIRST argument's own Equals, chosen by its type before C# boxed it and found by parameter, so Equals(objB: x, objA: y) asks y, both evaluated as written; a value of a type that refuses every other (a number, a char, a bool, an enum, a tuple, a string, an anonymous object) is unequal to one known to be another, so object.Equals((byte)1, 1) and object.Equals('a', "a") are false. Equals(object) inherited from object compares an array, a list, a StringBuilder and a class that does not override it by reference, and asks an override wherever one exists, as a call, a method group or a bare Equals(x) in the class. Equals and ToString through a null reference (a record, a class, a string, an interface, object) throw NullReferenceException after the argument is evaluated, where a hole and a concatenation of a null write nothing. GetType() and MemberwiseClone() fail the build (EQ2004), once for a chain read from one (x.GetType().Name) (Since 0.2.0-preview.64). A list, an array, a tuple or an enum held as object on both sides has lost the type .NET reads, since the browser holds the first three as one array and an enum as its member's key, so two lists or two arrays held so compare by their elements, as two tuples do, and a boxed enum equals its key's string.
Nullable<T> (T?)
✅
HasValue/Value, GetValueOrDefault() (type-aware default: 0/false/$eq.num.dec(0)/enum zero-member/…) and GetValueOrDefault(fallback), ??; lifted operators via $eq.nullable.*: arithmetic propagates null, relational (< > <= >=) is false when either side is null (not a numeric coercion); a compound assignment (+=, *=, …), an increment and a unary -, ~ or + on a nullable number are lifted too, a null staying null and a value following its type's rule (a float? rounds, a byte? wraps, a decimal? stays exact; Since 0.2.0-preview.58). A struct's own operators and conversions are lifted the same way, the operator running only over values: == finds two absent values equal and an absent one unequal to any value without it, a relation is false and every other operator null, and one declared over the nullable values themselves is called as written (Since 0.2.0-preview.64). Value on an empty one throws InvalidOperationException ("Nullable object must have a value."), as does every unwrap C# reads through it: a cast to the value's type or another, the value a struct's own conversion operator takes, and a foreach element of a type that is not nullable. Equals, Nullable.Equals and EqualityComparer<T>.Default.Equals answer as the lifted == does, two empty values equal and an empty one unequal to any value, and the ToString method group writes what its call writes. A struct's own Value, HasValue, GetValueOrDefault or Count, read through ?. or a property pattern, is the struct's (Since 0.2.0-preview.64). No-arg GetValueOrDefault() on DateTime?/Guid?/struct yields null, so use the fallback form there.
Guid
✅
Guid.NewGuid()→crypto.randomUUID(), Guid.Empty, Guid.Parse/TryParse/new Guid(text) → the canonical lowercase D text (Since 0.2.0-preview.60); string wire