Skip to content

Compiler fr

Edgar Mesquita edited this page Oct 14, 2026 · 1 revision

Le compilateur (CSharpToJs)

🌐 Cette page en : English · Português · Español · Français · Deutsch

Le compilateur est le composant central qui rend possible la magie d'eQuantic.UI. Il transforme la sémantique de C# en code TypeScript efficace et lisible.

La justesse est tenue par une suite différentielle de conformité : le même C# est évalué dans .NET et dans le Bun embarqué, et les deux réponses doivent s'accorder. Tout ce que le compilateur ne peut pas traduire fidèlement est une erreur de compilation avec un emplacement (les diagnostics EQ2xxx), jamais une fausseté silencieuse.

🛡️ Frontières (serveur contre client)

Pour garder le code du navigateur honnête, le compilateur impose des frontières strictes, inspirées de Next.js (la séparation serveur/client) et de Flutter (les contraintes), et les valide avant d'émettre du JS :

  • Composants client (StatefulComponent / StatelessComponent) : logique d'UI, gestion d'état, System.Linq, types de base (string, int, DateTime).
  • Interdit sur le client : System.IO, System.Net.Http direct, un .Wait() bloquant. File.ReadAllText() dans le corps d'un composant est une erreur de compilation.
  • Le pont : la récupération de données passe par des méthodes annotées [ServerAction] (style RPC). Voir Sécurité et Server Actions.

🛠️ Composants du compilateur

1. TypeScriptEmitter

Le TypeScriptEmitter est le point d'entrée pour générer des fichiers .ts. Il organise les imports, définit les classes et utilise le CSharpToJsConverter pour convertir les corps des méthodes.

2. CSharpToJsConverter

Il répartit chaque nœud Roslyn vers une stratégie, une par construction (BinaryExpressionStrategy, IfStatementStrategy, …), et renvoie de l'IR, pas du texte. Les instructions bâtissent toujours un JsStatement. Une stratégie d'expression qui a déjà traversé bâtit un JsExpr (IExpressionIrStrategy) ; une qui renvoie encore du texte est greffée comme un nœud opaque, identique octet pour octet à ce qu'elle a toujours produit. Cette frontière est ce qui laisse la migration avancer une stratégie à la fois, et tests/eQuantic.UI.Compiler.Tests/Coverage/ir-migration.baseline.txt est la liste des stratégies de texte : elle ne fait que rétrécir, et une nouvelle stratégie naît sur l'IR.

3. SourceMapGenerator et SourceMapComposer

SourceMapGenerator écrit une carte V3 standard (VLQ en Base64) depuis chaque module TypeScript qu'eqc émet vers les lignes .cs dont il est venu, en nommant chaque fichier depuis lequel le module a été écrit (une classe qui prend le default d'une interface mène ce default vers le fichier de l'interface), chacun dans le projet, et avec le C# lui-même dedans seulement quand la carte est full. Bun mappe ensuite le JavaScript vers ce TypeScript, et SourceMapComposer compose les deux, en C# et sans aucun paquet npm, en UNE carte du JavaScript que le navigateur exécute jusqu'au C#, en gardant le debugId de bun. Les deux cartes passent par un seul écrivain de cartes, et cet écrivain, comme chaque fichier JSON que la compilation écrit, par un seul écrivain JSON dont l'échappement est celui de System.Text.Json, donc un fichier C# qui contient un caractère de contrôle a quand même une carte. Ce que porte une carte (EQuanticSourceMaps : full en Debug, none ailleurs, external sur demande) est sur Débogage.

4. Les symboles d'abord, les noms seulement là où c'est honnête

Le convertisseur interroge le modèle sémantique avant de deviner. Un membre, une locale, un paramètre, un statique : chacun émet depuis son symbole (this.name, Counter.name, un nom nu), et Console.WriteLine est console.log parce que le symbole dit System.Console. Les heuristiques de nom (un tiret bas initial, une propriété capitalisée) ne sont légales que là où le modèle ne peut pas être interrogé : un extrait sans modèle, un nœud qu'une stratégie a réécrit. Sous un modèle qui fait autorité, un nom de l'arbre qui ne se lie pas est une erreur de compilation (EQ2006), jamais une traduction devinée.

5. L'IR et ses écrivains

Depuis 0.2.0-preview.36

CodeGen/Ir/ est un petit arbre avec un écrivain par niveau, JsExpr → JsStatement → JsClassMember → JsClass → JsModule, et les écrivains possèdent tout ce qu'une stratégie ne doit jamais écrire à la main :

  • Les parenthèses viennent de la précédence et de l'associativité, jamais d'un gabarit. Avant cela, f ?? g && g était livré tel quel : C# n'a besoin d'aucune parenthèse là, JavaScript refuse le mélange nu, et le bundle entier ne s'analysait pas.
  • Une seule évaluation : un JsTemplate nomme ce qu'il calcule ({0} === {0}.normalize()) et l'écrivain lie exactement une fois une partie utilisée plus d'une fois : un nom simple ou un littéral est inliné, une lecture de membre non (un getter peut compter). Une partie à l'intérieur d'une fonction que le gabarit définit est liée aussi, un nom simple compris, puisque cette fonction tourne une fois par élément : seuls un littéral, this et une lambda écrite sur place restent dedans, et de même une partie qui lit un nom que la fonction déclare.
  • La disposition : une instruction par ligne, les blocs indentés (JsLayout.Pretty) ; Compact reproduit l'ancien monde de chaînes octet pour octet, et c'est ainsi que chaque étape de la migration est prouvée. Une classe a une seule règle de disposition, une ligne vide avant un membre avec un corps, les champs contigus, et un module est ses imports comme des records JsImport, une ligne vide, son corps.
  • L'émetteur (TypeScriptEmitter) décide ce que contient un module et remet des nœuds au constructeur ; il n'assemble aucun texte.

Deux filets tiennent tout cela : les épinglages de composant (le module généré de chaque composant partagé, octet pour octet : un changement de disposition doit être uniquement d'espaces contre eux) et la suite de conformité, qui exécute chaque forme traduite des deux côtés et compare les réponses. Les jumeaux générés sous src/eQuantic.UI.Runtime/src/shared/components sont épinglés, vérifiés en types et testés, pas passés au linter : le code généré répond à son écrivain, pas à un guide de style.

🔄 Stratégies prises en charge

Actuellement, le compilateur prend en charge un large éventail de constructions C# :

  • Expressions : arithmétiques, logiques, ternaires, interpolation de chaînes, fusion de nuls (??), accès conditionnel (?., ?[]). Un argument qui attend derrière ?. est attendu là où il est écrit, seulement quand le receveur n'est pas nul (Depuis 0.2.0-preview.60), quel que soit le receveur : un appel, un élément ou une propriété devant ?. est lu une fois dans une variable de la méthode où il est écrit (Depuis 0.2.0-preview.61 ; jusque-là un tel receveur cassait la compilation avec EQ1004). Une lecture conditionnelle de nul répond null là où sa valeur est utilisée, comme C# le fait, jamais l'undefined de JavaScript (Depuis 0.2.0-preview.61). Un groupe de méthodes est le délégué que C# fait : son receveur est lu une fois, quand le délégué est fait, base.M appelle la méthode de la base sur cet objet, et le groupe d'une méthode d'extension est lié sur la classe qui la déclare, là où va son appel. Un groupe sur une extension que rien dans le bundle ne déclare, comme list.Any, casse la compilation avec EQ2004 (Depuis 0.2.0-preview.61)
  • Flux de contrôle : if, switch, for, foreach, while, do-while, break, continue, throw
  • Motifs modernes : prise en charge complète des motifs récursifs, de propriété, positionnels, relationnels et logiques (C# 9.0 à 12.0)
  • Gestion des ressources : prise en charge des instructions using et des déclarations using var
  • Exceptions : prise en charge complète de try-catch-finally et des instructions throw (Exception → Error)
  • Index et plages : prise en charge de l'opérateur d'index depuis la fin (array[^1] → array[array.length - 1])
  • Méthodes de chaîne : méthodes d'instance (Split, Replace, StartsWith, EndsWith, Contains, Substring, IndexOf, LastIndexOf, PadLeft, PadRight, Trim, TrimStart, TrimEnd, ToUpper, ToLower, ToUpperInvariant, ToLowerInvariant, Insert, Remove, ToCharArray) et méthodes statiques (IsNullOrEmpty, IsNullOrWhiteSpace, Join, Concat, Compare, Equals, Format). Une méthode à laquelle on donne un StringComparison répond par lui, par le runtime, et une recherche par une comparaison de culture casse la compilation (EQ1004) : le navigateur n'a pas de recherche consciente de la culture (Depuis 0.2.0-preview.60)
  • Méthodes numériques : Parse et TryParse sur chaque type numérique (int, uint, long, ulong, short, ushort, byte, sbyte, float, double, decimal), lus par le runtime avec la grammaire de .NET
  • Méthodes de List : Add, AddRange, Insert, InsertRange, Remove, RemoveAt, RemoveRange, RemoveAll, Clear, IndexOf, LastIndexOf, Find, FindIndex, FindLast, FindLastIndex, FindAll, Exists, TrueForAll, Sort, ForEach, GetRange, CopyTo, BinarySearch
  • Faces de List : un élément lu ou écrit par IList<T>, IReadOnlyList<T> ou IList → $eq.collections.item(list, i) et $eq.collections.setItem(list, i, value), son Count → $eq.collections.count(list), et l'Add et le Clear propres d'ICollection<T> → $eq.collections.add(collection, item) et $eq.collections.clear(collection). Chacun répond pour la collection que la face tient quand il tourne : un tableau par son indice, l'ensemble, la liste chaînée ou le dictionnaire du runtime par ses propres membres, et un type à vous par son indexeur, son Count, son Add et son Clear (Depuis 0.2.0-preview.61)
  • Méthodes statiques d'Array : prise en charge complète des méthodes statiques d'Array
    • Array.Sort(array) → array.sort() - Trie le tableau sur place
    • Array.Sort(array, comparison) → array.sort(comparison) - Trie avec un comparateur à soi
    • Array.Reverse(array) → array.reverse() - Inverse le tableau sur place
    • Array.Find(array, predicate) → array.find(predicate) - Trouve le premier élément qui correspond
    • Array.FindIndex(array, predicate) → array.findIndex(predicate) - Trouve l'index de la première correspondance
    • Array.FindAll(array, predicate) → array.filter(predicate) - Trouve tous les éléments qui correspondent
    • Array.IndexOf(array, value) → array.indexOf(value) - Trouve l'index d'une valeur
    • Array.LastIndexOf(array, value) → array.lastIndexOf(value) - Trouve le dernier index d'une valeur
    • Array.Exists(array, predicate) → array.some(predicate) - Vérifie si un élément correspond
    • Array.TrueForAll(array, predicate) → array.every(predicate) - Vérifie si tous les éléments correspondent
    • Array.Clear(array) → array.splice(0) - Retire tous les éléments
    • Array.Resize(ref array, size) → array.length = size - Redimensionne le tableau
  • Méthodes d'Enum : un enum n'a pas d'objet à lui dans le navigateur, donc les statiques d'Enum appellent les fonctions d'enum du runtime, $eq.enums, avec la forme de l'enum écrite à l'appel : ses noms, les noms camelCase que le navigateur tient, ses valeurs et s'il est un enum [Flags] (Depuis 0.2.0-preview.60 ; elles nommaient un objet qu'aucun module ne déclare, et levaient)
    • Enum.Parse<T>(text) et Enum.Parse(typeof(T), text) → $eq.enums.parse(text, shape) : un nom, des noms joints par des virgules ou un nombre, la casse gardée sauf si ignoreCase est vrai, et une erreur là où .NET en lève une
    • Enum.TryParse<T>(text, out var result) → vrai avec la valeur, ou faux avec le défaut de l'enum dans result, et null pour la surcharge qui prend un Type
    • Enum.GetName(value) → le nom du membre qui a cette valeur, ou null là où aucun ne l'a
    • Enum.GetNames<T>() et Enum.GetValues<T>() → les noms et les valeurs, dans l'ordre des valeurs
    • Enum.IsDefined(typeof(T), value) → si un membre a cette valeur, donnée comme l'enum, un nombre, un nom déclaré, ou un object qui en tient un
    • value.ToString(), une interpolation et une concaténation écrivent le nom du membre, les drapeaux posés d'une combinaison de flags (Read, Write), les chiffres d'une valeur qu'aucun membre ne nomme, et rien pour un enum nullable nul. ToString("D"), "X", "F" et "G", et le format d'une interpolation, écrivent ce que .NET écrit, et un alignement complète ce texte
    • Un cast depuis un object qui tient l'enum le garde ((Status)Enum.Parse(typeof(Status), text)), un cast entre deux enums passe par leurs valeurs, et une valeur qu'aucun membre ne nomme est tenue comme son nombre
  • Méthodes de Dictionary : chaque Dictionary, IDictionary et IReadOnlyDictionary, et les triés, est une classe du runtime sous $eq.collections.*, interrogée avec has/get (Depuis 0.2.0-preview.60)
    • new Dictionary<K, V>() → $eq.collections.dictionary(), ou $eq.collections.dictionary(null, true) là où le type de la clé compare par valeur (un record, un struct, un tuple, un decimal, une date, une classe qui surcharge Equals) ; une copie l'amorce avec sa source, et un initialiseur avec des paires [key, value]
    • dict[key] → $eq.mapGet(dict, key) - lève pour une clé manquante comme .NET le fait ; dict[key] = value → $eq.mapSet(dict, key, value), qui répond la valeur écrite
    • ContainsKey(key) → dict.has(key) - ne répond jamais pour un membre du prototype
    • TryGetValue(key, out var value) → (dict.has(key) ? ((value = dict.get(key)), true) : ((value = 0), false)) - un échec écrit default(TValue) (0 ici, pour un int)
    • GetValueOrDefault(key[, defaultValue]) → (dict.has(key) ? dict.get(key) : 0) - un défaut explicite est évalué une fois, qu'il soit nécessaire ou non
    • Un receveur ou une clé lus par un membre ou un appel sont liés une fois : this.cache.TryGetValue(Next(), out var v) → (($0, $1) => …)(this.cache, next())
    • Add(key, value) → dict.set(key, value) - ne lève PAS pour une clé déjà présente, là où celui de .NET lève (#440)
    • Remove(key) → dict.delete(key), et Remove(key, out var value) déplace la valeur dans l'out
    • TryAdd(key, value) → dict.tryAdd(key, value), ContainsValue(value) → dict.containsValue(value)
    • Clear() → dict.clear()
    • Keys / Values (propriétés) → dict.keys() / dict.values(), des tableaux dans l'ordre du dictionnaire ; Count, Keys.Count et Values.Count → dict.size, et Keys.Contains(key) → dict.has(key)
  • LINQ : conversion directe des méthodes LINQ vers leurs équivalents JS :
    • Projection : Select → map, SelectMany → flatMap
    • Filtrage : Where → filter, Distinct → [...new Set()]
    • Ordonnancement : OrderBy/OrderByDescending → sort, Reverse → [...arr].reverse()
    • Partitionnement : Skip → slice(n), Take → slice(0, n)
    • Élément : First/FirstOrDefault → find/[0], Last/LastOrDefault → arr[arr.length-1], Single/SingleOrDefault → find/[0] ; avec une valeur par défaut, FirstOrDefault, LastOrDefault et SingleOrDefault → $eq.linq.firstOrDefault, $eq.linq.lastOrDefault et $eq.linq.singleOrDefault, qui la répondent là où rien ne correspond (Depuis 0.2.0-preview.62)
    • Quantificateurs : Any → some/length > 0, All → every, Contains → includes
    • Agrégation : Count → length/filter().length, Sum → reduce(($a, $b) => $a + $b, 0), Average → $eq.linq.average, qui calcule la moyenne dans le type de l'élément, un long, un float, un decimal ou un nullable (Depuis 0.2.0-preview.62), Min → Math.min(...), Max → Math.max(...)
    • Opérations d'ensemble :
      • Concat(other) → [...source, ...other] - Concatène deux séquences
      • Union(other) → [...new Set([...source, ...other])] - Éléments uniques des deux séquences
      • Intersect(other) → [...new Set(source)].filter(($x) => other.includes($x)) - Éléments communs
      • Except(other) → [...new Set(source)].filter(($x) => !other.includes($x)) - Éléments de la source mais pas de l'autre
      • other, comme chaque argument d'un appel LINQ qui n'est pas une lambda écrite sur place, est évalué une fois, dans l'ordre de C#, avant que l'abaissement ne tourne : il tournait une fois par élément (Depuis 0.2.0-preview.61)
    • Paires : Zip(second) et Zip(second, third) → $eq.linq.zipTuples, qui répond les paires et les triplets que .NET répond (Depuis 0.2.0-preview.62)
    • Égalité : SequenceEqual → $eq.linq.sequenceEqual, qui compare par l'égalité par défaut du type de l'élément (Depuis 0.2.0-preview.62)
    • Comparateurs : un comparateur remis à ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet ou SequenceEqual qui demande celui par défaut (EqualityComparer<T>.Default, null, StringComparer.Ordinal) est écarté, puisque l'abaissement trouve déjà ses clés ainsi, et tout autre casse la compilation avec EQ2007 (Depuis 0.2.0-preview.61)
    • Filtrage par type :
      • Cast<T>() → passage direct (JavaScript est à typage dynamique)
      • OfType<T>() → filter(($x) => typeof $x === 'type') pour les primitifs, filter(($x) => $x instanceof Type) pour les objets
  • Async/Await : mise en correspondance de Task avec Promise et prise en charge native d'await.
  • Opérateurs C# modernes : prise en charge des opérateurs et des mots-clés modernes de C#
    • Affectation avec fusion de nuls : x ??= value → x ?? (x = value) - Affecte seulement si nul ou undefined
    • Opérateur nameof : nameof(variable) → 'variable' - Obtient le nom comme chaîne à la compilation. C'est le nom, pas son orthographe : nameof(@class) → 'class' (Depuis 0.2.0-preview.60)
    • Mot-clé default : default(T) et le littéral default sont le défaut du type, le type du littéral étant celui vers lequel C# le convertit : default(int) → 0, default(long) → 0n, default(int?) → null, int x = default → 0 (Depuis 0.2.0-preview.58)

Ce que C# vous donne gratuitement, et que JavaScript ne donne pas

Deux valeurs par défaut sont implicites en C# et absentes en JavaScript. Les deux ont été émises comme rien pendant un temps, et les deux échouent TARD, pas à la compilation, et pas là où est la cause.

Un type valeur non posé vaut ZÉRO

Depuis 0.2.0-preview.22

Un champ d'un type valeur vaut zéro que quelqu'un ait écrit = 0 ou non. Sur le client c'était undefined, et les deux ne sont pas la même valeur. Les lectures survivent par chance aussi longtemps qu'elles sont des TESTS (undefined > 0 est faux, ce que 0 aurait dit), et puis la première ARITHMÉTIQUE le transforme en NaN. Math.max(width, undefined) atteint la feuille de styles comme width:NaNpx, une règle que l'analyseur CSS jette entière : la classe est calculée, hachée, émise, posée sur l'élément, et ne fait rien.

Cela n'apparaît que sur les pages RENDUES côté client, jamais sur du SSR ni sur un chargement direct, parce que le serveur calcule la même propriété en C# où elle valait 0 depuis le début. Donc les deux cibles sont en désaccord sur un champ et la page qui le prouve est celle que personne ne recharge.

Chaque type valeur non nullable porte désormais son défaut (0, false, le membre zéro d'un enum). Les nullables non, parce que là null EST la réponse de C#, et inventer un zéro serait la même divergence pointant dans l'autre sens.

Un paramètre de constructeur primaire est de l'ÉTAT d'instance

Depuis 0.2.0-preview.21

public sealed class TocEntry(Action<string, bool> onSeen, string id) : StatelessComponent
{
    public override VisualNode Build(ComponentContext context) =>
        new InView(Heading(id), visible => onSeen(id, visible));   // this.onSeen, this.id
}

Roslyn modélise la capture comme un IParameterSymbol, donc chaque endroit qui demande « est-ce un paramètre ? » répond oui à propos de quelque chose qui se comporte comme un champ. Émis nu cela compile, la page se rend, et le ReferenceError arrive quand le callback finit par se déclencher, en remontant depuis l'intérieur du réconciliateur comme un TypeError à propos de tout autre chose.

Les modèles ordinaires traversent aussi

Un composant n'est pas le seul C# dont une page a besoin. Le modèle de document derrière un éditeur, une petite machine à états, un analyseur : aucun d'eux n'est un composant, et tous doivent tourner sur les deux cibles. Ils transpilent de la même façon, comme leurs propres modules, et les règles ci-dessous sont ce qui rend l'émission VÉRIFIÉE plutôt que simplement présente.

Les plages sont des tranches

line[start..end]   →  line.slice(start, end)
line[2..]          →  line.slice(2)
line[..^1]         →  line.slice(0, -1)
line[..^n]         →  $eq.slice(line, 0, false, n, true)

La dernière forme est celle que JavaScript ne peut pas dire directement : ^0 signifie la FIN, tandis que slice(0, -0) est slice(0, 0), qui est vide. Tout ce qui n'est pas un littéral positif après ^ se résout donc contre la longueur comme Index.GetOffset le fait. Une Range gardée comme VALEUR est signalée (EQ2004) : rien de l'autre côté n'en reçoit une, et indexer au point d'usage est à quoi elle sert.

Une plage sur un type à vous avec un Length (ou un Count) et un Slice(int start, int length) n'est pas la tranche d'un tableau : C# l'abaisse vers ce Slice, qui prend un début et une LONGUEUR là où le slice de JavaScript prend une fin, et le jumeau fait de même (Depuis 0.2.0-preview.61) :

strip[1..3]    →  strip.slice(1, 3 - 1)

Le receveur est lu une fois, puis les extrémités dans l'ordre où elles sont écrites, puis le Length ou le Count, lu seulement là où une extrémité compte depuis la fin ou la fin est laissée ouverte, comme .NET les lit. Une plage remise à un indexeur qui prend la Range elle-même lui remettrait une valeur Range, et casse la compilation (EQ2004) pour la même raison qu'une plage gardée.

out et ref

JavaScript n'a ni l'un ni l'autre. Une méthode qui les déclare renvoie un OBJET (sa propre valeur sous $, chaque out et ref sous son nom) et son corps passe dans une fermeture pour que chaque return dedans garde le sens qu'il avait. Le site d'appel déballe avec une flèche, ce qui marche dans n'importe quelle position d'expression, y compris dans un if :

var next = document.Replace(range, text, out var caret);
let caret: any;
let next = ($o => (caret = $o.caret, $o.$))(document.replace(range, text));

out quitte la liste de paramètres JS (il n'est pas passé VERS l'intérieur) ; ref reste, parce qu'il est lu avant d'être écrit. out _ n'affecte rien.

Les variables d'expression sont déclarées là où C# les déclare

La liaison d'un motif (x is T t), un out var et l'élément d'une déconstruction ((var c, var d) = …) sont affectés dans l'expression qui les déclare, donc l'instruction qui tient cette expression les déclare, avec la portée que Roslyn leur donne (Depuis 0.2.0-preview.59) :

  • une instruction d'expression, un if, un return, un throw, un yield return, une déclaration, l'expression gouvernante d'un switch et un lock les déclarent devant eux-mêmes, là où le code après eux peut les lire (if (!int.TryParse(s, out var n)) return; puis n). Posés directement dans une section de switch, ils les laissent au switch, qui les déclare une fois pour tout son bloc, comme C# les porte, donc une autre section peut les affecter ;
  • un while, un do et un for les déclarent dans la tête propre de la boucle, for (let n; …), que JavaScript copie pour chaque itération, comme .NET donne à la condition d'une boucle une variable fraîche : une fermeture faite dans une itération garde la valeur de cette itération ;
  • un foreach et un using les déclarent dans un bloc autour de l'instruction, qui est leur portée en C# ;
  • une lambda, une fonction locale, la clause d'une requête et le bras d'une expression switch déclarent les leurs, une fois par appel ;
  • l'initialiseur d'un champ ou d'une propriété les déclare dans une flèche à lui.

Un nom que JavaScript refuse, et que l'échappement verbatim de C# permet (@class, @new, @default), devient un nom légal à sa déclaration et à chaque référence, de quelque façon qu'il ait été lié : class$, avec un $ qu'aucun nom C# ne peut tenir. Une étiquette et un paramètre de type d'une méthode écrits ainsi prennent le même nom, et un membre ou une clé de with est le nom lui-même : r.class, { class: 5 } (Depuis 0.2.0-preview.61).

Une locale nommée comme un global que le code émis lit (crypto, undefined, Math, Number, console) prend le même $, donc le global reste visible pour le code à côté d'elle, et chaque nom que le code émis déclare pour lui-même commence par un $, donc il ne rencontre jamais une locale que le code autour de lui lit (Depuis 0.2.0-preview.61).

Collections : la capacité n'est pas le contenu

new List<T>(x) signifie deux choses opposées selon ce qu'est x, et seul le constructeur résolu peut dire laquelle : new List<string>(other.Count) est une liste vide dimensionnée à l'avance, new List<string>(other) est une copie. La première émet un tableau vide, la seconde [...other]. Avec un initialiseur de collection, la liste est ce que son constructeur y a mis, puis chaque élément dans l'ordre : new List<string>(other) { "x" } est [...other, "x"] et new List<string>(8) { "x" } est ["x"], écrit ou typé par la cible. Une capacité qui n'est pas une constante que la liste prend est évaluée quand même, avant les éléments, et une négative est refusée comme le constructeur de List<T> la refuse : new List<string>(Size()) { "x" } est ($eq.collections.listCapacity(this.size()), ["x"]) (Depuis 0.2.0-preview.61).

L'arithmétique sur les chars calcule sur les unités de code

Un char C# dans + - * / % promeut en int et calcule sur l'unité de code, tandis qu'un char transpilé est une chaîne de longueur 1. Quand le type du RÉSULTAT est numérique, les opérandes char s'abaissent en unités de code (les littéraux constants se replient en nombre ; les expressions lisent charCodeAt(0)), donc text[i] - '0' est le chiffre et 'A' + col est un nombre, exactement comme dans .NET. (char)numeric s'abaisse en String.fromCharCode. char + string reste une concaténation : son type de résultat est string, donc la branche numérique ne le voit jamais.

Une valeur du serveur traverse une frontière TYPÉE

JavaScript n'a pas de decimal et pas d'entier 64 bits, donc le câble envoie ceux-là comme des chaînes, un long comme "9007199254740993", un decimal comme "0.1", la famille des dates comme du texte ISO. Le compilateur connaît le type C# de chaque champ d'état et de chaque retour de Server Action, donc il écrit ce savoir dans le jumeau comme une carte statique $hydration, et le runtime convertit UNE FOIS, là où la valeur arrive, au lieu que chaque site d'usage convertisse par précaution. Un record nomme sa propre carte, une liste est [spec], les valeurs d'un dictionnaire sont { dict: spec }, et un tuple est positionnel ({ tuple: [...] }) parce qu'il traverse comme un tableau. Rien n'est émis là où l'hydratation serait l'identité, ce qui est la plupart des champs.

Un défaut est décidé par le TYPE, et n'est jamais nul pour un type valeur

new int[0].SingleOrDefault() vaut 0 dans .NET, pas null, et il en va de même pour FirstOrDefault, LastOrDefault, ElementAtOrDefault et DefaultIfEmpty. Un champ déclaré sans initialiseur prend le même défaut : un int vaut 0, un bool faux, un long 0n, et un ENUM son membre de valeur zéro, qui est une chaîne portant le NOM du membre de ce côté, donc un champ laissé non posé ne rendrait sinon rien du tout. Une seule table répond pour les deux, par symbole, donc un type atteint par un alias (using Amount = decimal;) n'est pas une réponse différente du type lui-même.

La même table remplit un tableau dimensionné, new T[n], et répond default(T) et le littéral default, dont le type est celui vers lequel C# le convertit : celui d'une locale, d'un paramètre, de l'autre bras d'une conditionnelle. Un élément struct est une instance zéro à lui, puisque fill partagerait un objet sur chaque case, et les jumeaux de temps et d'identité (DateTime, TimeSpan, DateOnly, TimeOnly, DateTimeOffset, Guid) partent de leur MinValue, Zero ou Empty. Un struct dont le jumeau écrit à la main ne peut pas bâtir son zéro garde le défaut propre du jumeau dans un littéral default : undefined, que le constructeur du jumeau transforme en son défaut, là où null le contournerait. Depuis 0.2.0-preview.58.

Un second OrderBy recommence l'ordonnancement

xs.OrderBy(a).OrderBy(b) trie par b. L'ordonnancement précédent ne reste pas la clé primaire : il ne survit que comme le départage qu'un tri stable lui donne, donc c'est un résultat différent de xs.OrderBy(a).ThenBy(b) dès que b a des égalités que a départagerait. Les deux traversent fidèlement : un OrderBy enchaîné trie sa source d'abord et trie cela, et ThenBy compose sa clé dans la même comparaison.

Une moitié arrondit vers le voisin PAIR, comme .NET

Math.round(32.5) vaut 33 en JavaScript et 32 dans .NET, qui arrondit une moitié vers le pair. Partout où le runtime reflète un MathF.Round, il utilise la règle de .NET, donc une valeur calculée dans le navigateur est la valeur que le serveur a calculée : c'est ce qui garde l'interligne d'une échelle typographique, et toute mise en page dérivée d'une, identique à travers l'hydratation plutôt qu'à une fraction près.

C'est l'arithmétique de .NET, pas une approximation d'elle. Le point milieu est EXACT : Math.Round(0.5015, 3) vaut 0,501, parce que 0.5015 * 1000 vaut 501,49999999999994 : une valeur près d'une moitié n'est pas une moitié. Chaque mode de MidpointRounding est honoré, un compte de chiffres au-delà de 15 (6 pour un float) lève comme celui de .NET, et MathF.Round(x, digits) met à l'échelle et divise en simple précision.

La surface LINQ est une table, et la BCL numérique en est une autre

La plupart des opérateurs LINQ font une forme chacun (Where est filter, Aggregate est reduce avec ses arguments échangés), donc ils vivent comme des entrées dans une table indexée par opérateur et nombre d'arguments, gardée une fois, avec l'écrivain d'IR qui ponctue et lie tout receveur utilisé deux fois. Un opérateur qui doit RAISONNER garde une stratégie à lui : la famille OrDefault lit le type de l'élément, Sum choisit une graine d'après lui, Cast et OfType testent des types, Contains choisit entre égalité d'identité et structurelle. La même forme vaut pour la BCL numérique, où la surface moderne (Double.AcosPi et sa famille) est une table de gabarits plutôt qu'une méthode chacune.

Là où ces tables n'ont pas d'entrée, l'appel est une ERREUR de compilation qui nomme le membre, jamais une supposition : un nom émis sans rien derrière est un ReferenceError au chargement, et une page qui ne se rend jamais.

Les entiers de largeur fixe se règlent par leur type

Un byte C# au-delà de 255 boucle ; un nombre JavaScript continue de compter. Le compilateur lit le type du RÉSULTAT de chaque + - * << (et de ++, -- et des formes composées) et règle la valeur là où C# le ferait : byte, sbyte, short, ushort et uint bouclent TOUJOURS (& 0xFF, << 16 >> 16, >>> 0 : les valeurs empaquetées et les hachages en dépendent, donc h *= 16777619 sur un uint est le pas FNV qu'il est dans .NET, via Math.imul). int et long ne bouclent que là où vous avez écrit unchecked (unchecked(a * a) vaut 0 pour 65536 ; un long boucle via BigInt.asIntN), parce que chaque i + 1 ordinaire dans une UI porterait sinon un | 0 pour un débordement qui est un bug partout ailleurs, donc un int.MaxValue + 1 ordinaire garde le compte du double, une limite documentée. Un contexte checked (le bloc, l'expression, ou le réglage de tout le projet, lu depuis l'IsChecked de l'arbre lié plutôt que depuis la syntaxe) lève un débordement exactement là où C# le lève. Un float est un simple partout où il est PRODUIT, chaque + - * /, chaque incrément et affectation composée sur une cible float (une float? n'est pas encore arrondie), un int au-delà de 2^24 sur son chemin vers l'intérieur, une constante float, une valeur hydratée du serveur, parce que RyuJIT arrondit chaque opération float, et ainsi a*x - b*x répond le dernier bit du serveur dans le navigateur aussi. Il s'imprime comme le plus court décimal qui se relit comme le même simple, donc 0.1f + 0.2f vaut "0.3" des deux côtés. char++ avance le caractère ; un enum dans une arithmétique (day + 1, a.CompareTo(b)) calcule sur la valeur derrière le nom du membre.

Les conversions implicites sont réglées par l'arbre lié

La syntaxe ne montre jamais une conversion implicite (int i = c avec un char, Twice(c), a[c], long l = n) et une règle écrite contre la syntaxe ne couvre que les formes dont son auteur s'est souvenu. Le compilateur les lit depuis l'arbre lié de Roslyn à la place : après que chaque expression est traduite, la conversion dans laquelle l'arbre lié l'enveloppe est appliquée (ValueFlow), à chaque site où C# l'applique : initialiseurs, arguments, index, retours, comparaisons. Un char promu en nombre devient son unité de code (une constante se replie : 'A' + col est 65 + col) ; un int qui coule vers un long devient un BigInt (1 → 1n, ce qui est aussi pourquoi TimeSpan.FromSeconds(90) émet 90n : la surcharge de .NET 9 prend un long). Deux chars comparés restent des caractères : JavaScript ordonne les chaînes de longueur 1 par les mêmes unités de code. Une conversion définie par l'utilisateur traverse comme un appel au statique que son jumeau porte, un type propre à une app comme Money.fromInt(5), un type du vocabulaire comme IconGlyph.fromIcons('search') pour Icon(Icons.Search), sauf si son jumeau prend l'opérande tel quel : SizeValue depuis un nombre déclare exactement cela, donc Width = 120 reste un 120 ordinaire, et un type venu de l'extérieur du SDK (Index) passe son primitif directement. Une valeur sur son chemin vers du TEXTE est réglée de la même façon : encadrée dans une concaténation, un opérande chaîne de l'une, ou un trou d'interpolation ordinaire, elle s'imprime comme C# l'imprime, un nul comme rien, un bool comme "True", un enum par le nom de son membre, s += flag compris, qu'aucune règle de syntaxe n'avait vu.

Les opérateurs propres d'un type traversent

JavaScript ne peut pas surcharger +, donc un record ou un struct que vous déclarez avec des opérateurs porte chacun dans son jumeau comme une méthode statique, et chaque site où l'arbre lié montre un opérateur l'appelle : a + b est Money.opAdd(a, b), -m est Money.opNegate(m) (le - unaire et le binaire sont nommés par arité, donc ils n'entrent jamais en collision), m += other est m = Money.opAdd(m, other). Les conversions aussi : implicit operator Money(int v) devient Money.fromInt, appelé partout où C# convertit, une déclaration, un argument, l'opérande d'un composé, et explicit operator int(Money m) devient Money.toInt, appelé par le cast. Seulement pour les types de votre source : une enveloppe du framework comme SizeValue ou Index est son primitif de ce côté, et ses opérateurs passent la valeur directement.

L'ÉLÉMENT d'un foreach se convertit de la même façon, un élément à la fois : foreach (long l in ints) fait de chaque int un BigInt, foreach (int code in chars) de chaque char son unité de code, foreach (Money m in ints) appelle la conversion du type : la boucle lie un $l brut et déclare l depuis lui. Une DÉCLARATION using (using var r = …;) possède le reste de son bloc : ce qui suit tourne dans un try dont le finally libère r, après qu'une valeur de retour est prise, quand le corps lève, dans l'ordre inverse quand plusieurs partagent un bloc, et await using attend disposeAsync.

Les dictionnaires s'énumèrent comme ceux de .NET

Un dictionnaire est la classe de dictionnaire du runtime, qui tient ses entrées par case comme le Dictionary de .NET : il s'énumère dans l'ordre d'insertion tant que rien n'est retiré, et une entrée ajoutée après un retrait prend la case de celle retirée, la dernière libérée en premier. foreach sur un (et new List<KeyValuePair<,>>(dict), qui l'étale) produit des paires qui se déstructurent comme [key, value] ET répondent .key/.value (les deux formes de consommation de C#), chaque clé dans son propre type, donc le key + 1 suivant additionne. Un dictionnaire trié produit les mêmes paires, dans l'ordre des clés. Depuis 0.2.0-preview.60.

Les annotations d'élément générique cèdent à l'inférence

new List<KeyValuePair<int,float>>(…) ne peut pas annoter son let avec le nom C# nu (KeyValuePair[] ne nomme rien en TS) ; les éléments génériques laissent l'annotation à l'inférence.

Les statiques s'initialisent comme un TYPE, au premier usage

C# initialise les statiques d'un type ensemble : chacun part de son zéro, puis les initialiseurs tournent dans l'ordre de déclaration, puis le constructeur statique, une fois, au premier usage. Un type dont les statiques peuvent s'observer l'un l'autre (un initialiseur qui n'est pas une constante, ou un constructeur statique) les tient tous dans un seul objet $slots qu'$init() bâtit la première fois que l'un d'eux est lu ou écrit, et chaque statique est une paire d'accesseurs sur sa case, donc static int A = B + 1; au-dessus de static int B = 2; lit le zéro de B et répond 1, comme .NET le fait, et deux types dont les statiques se lisent l'un l'autre voient le zéro que .NET voit. Le même mécanisme sert un record, un struct, une classe, une classe statique et un composant. Étant paresseux, il survit aussi aux cycles d'import de la bibliothèque partagée (ses modules s'importent l'un l'autre par un seul tonneau, et celui qui charge en premier voit la classe de l'autre comme undefined), et c'est à cela que servait le getter paresseux par statique qu'il remplace ; ce getter initialisait chaque statique à sa propre première lecture, dans aucun ordre. Un type dont les initialiseurs sont des constantes garde des champs ordinaires, chacun écrit comme la VALEUR vers laquelle C# le replie (static readonly int Max = Default * 2; au-dessus de const int Default = 50; est 100, là où l'expression lisait Default avant que sa déclaration ne le définisse), et un statique dont le zéro bâtit un jumeau est bâti au premier usage aussi, jamais pendant que son module est évalué. Un type qui déclare un constructeur statique démarre aussi l'initialisation d'abord dans son constructeur d'instance et dans chaque méthode, accesseur et opérateur statiques, puisque C# fait tourner ce constructeur avant la première instance et le premier usage de tout membre statique. Les statiques s'initialisent dans l'ordre de déclaration à travers les champs, les propriétés et les événements de type champ ; $slots existe avant que rien ne tourne, donc une lecture réentrante voit le zéro que C# voit ; le corps du constructeur statique tourne dans une fonction à lui après les initialiseurs, donc un return dedans le termine seul ; et un échec est gardé, chaque usage du type levant alors une TypeInitializationException dont l'exception interne est l'originale, comme .NET le fait. Depuis 0.2.0-preview.61.

Ce qu'une ANNOTATION de type peut nommer

Le .ts émis est vérifié en types (c'est la seconde des deux couches), donc une signature ne doit jamais introduire un nom que le module ne peut pas résoudre :

C# TypeScript
un enum string, puisque sa représentation à l'exécution est le nom du membre
une interface sans jumeau émis any
IReadOnlyList<(char, char)> [string, string][]
Action<T>? ((t: T) => void) | null, entre parenthèses, sinon l'union se lie au retour
char string
un nom que rien ne peut vérifier any, parce qu'un mauvais type est pire qu'un type ouvert

Les records portent les mêmes règles, plus leurs champs statiques et leurs propriétés calculées (un record est une valeur avec un COMPORTEMENT, pas seulement ses membres positionnels).

La bibliothèque EST son répertoire

L'ensemble transpilé et le tonneau d'exportation du runtime sont tous deux générés depuis le répertoire de sources, jamais depuis un registre tenu à la main, donc la bibliothèque embarquée ne peut jamais s'écarter du code dont elle est bâtie.

📝 Exemple de conversion

Source C# :

private void Increment() {
    Count++;
    if (Count > 10) Console.WriteLine("Max reached");
}

Sortie TypeScript :

increment() {
    this.count++;
    if (this.count > 10) console.log("Max reached");
}

🎯 Exemples de fonctionnalités avancées

Opérations sur les enums

Source C# :

public enum OrderStatus { Pending, Processing, Shipped, Delivered }

private void HandleStatusChange(string input)
{
    // Parse enum from string (case-insensitive)
    if (Enum.TryParse<OrderStatus>(input, ignoreCase: true, out var status))
    {
        Console.WriteLine($"Status changed to: {status}");
    }

    // Get all enum values for dropdown
    var allStatuses = Enum.GetValues<OrderStatus>();
    foreach (var s in allStatuses)
    {
        Console.WriteLine($"Available status: {s}");
    }

    // Validate enum value
    if (Enum.IsDefined(typeof(OrderStatus), "Shipped"))
    {
        Console.WriteLine("Valid status");
    }
}

Sortie TypeScript (shape représente la forme de l'enum, qu'eqc écrit à chaque appel : { names: ['Pending', 'Processing', 'Shipped', 'Delivered'], keys: ['pending', 'processing', 'shipped', 'delivered'], values: [0, 1, 2, 3], flags: false, digits: 8 }):

handleStatusChange(input: string) {
    let status: any;
    // Parse with TryParse: the enum's default when it fails, as .NET leaves it
    if (((status = $eq.enums.tryParse(input, shape, true)) !== undefined || ((status = $eq.enums.zero(shape)), false))) {
        console.log(`Status changed to: ${$eq.enums.text(status, shape)}`);
    }

    // Get all values, in the order of the values
    let allStatuses = $eq.enums.values(shape);
    for (const s of allStatuses) {
        console.log(`Available status: ${$eq.enums.text(s, shape)}`);
    }

    // Validate a declared name
    if ($eq.enums.isDefined('Shipped', shape, 'name')) {
        console.log('Valid status');
    }
}

Opérations sur les dictionnaires

Source C# :

private Dictionary<string, int> _settings = new();

private void ManageSettings()
{
    // Add entries
    _settings.Add("timeout", 5000);
    _settings.Add("retries", 3);

    // Check existence
    if (_settings.ContainsKey("timeout"))
    {
        var timeout = _settings["timeout"];
        Console.WriteLine($"Timeout: {timeout}");
    }

    // Safe retrieval
    if (_settings.TryGetValue("maxItems", out var max))
    {
        Console.WriteLine($"Max: {max}");
    }

    // Iterate keys
    foreach (var key in _settings.Keys)
    {
        Console.WriteLine($"{key} = {_settings[key]}");
    }

    // Clear all
    _settings.Clear();
}

Sortie TypeScript :

constructor() {
    this._settings = $eq.collections.dictionary();
}

declare _settings: any;

manageSettings() {
    let max: any;
    this._settings.set('timeout', 5000);
    this._settings.set('retries', 3);
    if (this._settings.has('timeout')) {
        let timeout = $eq.mapGet(this._settings, 'timeout');
        console.log(`Timeout: ${timeout}`);
    }
    if ((($0: any) => ($0.has('maxItems') ? ((max = $0.get('maxItems')), true) : ((max = 0), false)))(this._settings)) {
        console.log(`Max: ${max}`);
    }
    for (const key of this._settings.keys()) {
        console.log(`${key ?? ''} = ${$eq.mapGet(this._settings, key)}`);
    }
    this._settings.clear();
}

Opérations d'ensemble LINQ

Source C# :

private void ProcessCollections()
{
    var list1 = new[] { 1, 2, 3, 4 };
    var list2 = new[] { 3, 4, 5, 6 };

    // Concatenate two lists
    var combined = list1.Concat(list2);
    // Result: [1, 2, 3, 4, 3, 4, 5, 6]

    // Union - unique elements from both
    var union = list1.Union(list2);
    // Result: [1, 2, 3, 4, 5, 6]

    // Intersect - common elements
    var common = list1.Intersect(list2);
    // Result: [3, 4]

    // Except - elements in list1 but not in list2
    var difference = list1.Except(list2);
    // Result: [1, 2]

    // Complex filtering with set operations
    var activeUsers = GetActiveUsers();
    var premiumUsers = GetPremiumUsers();

    // Users that are both active AND premium
    var activePremium = activeUsers.Intersect(premiumUsers);

    // Users that are active but NOT premium
    var activeFree = activeUsers.Except(premiumUsers);
}

Sortie TypeScript :

processCollections() {
    const list1 = [1, 2, 3, 4];
    const list2 = [3, 4, 5, 6];

    // Concatenate
    const combined = [...list1, ...list2];

    // Union (with Set to remove duplicates)
    const union = [...new Set([...list1, ...list2])];

    // Intersect (common elements)
    const common = [...new Set(list1)].filter(x => list2.includes(x));

    // Except (difference)
    const difference = [...new Set(list1)].filter(x => !list2.includes(x));

    // Complex filtering
    const activeUsers = this.getActiveUsers();
    const premiumUsers = this.getPremiumUsers();

    const activePremium = [...new Set(activeUsers)].filter(x => premiumUsers.includes(x));
    const activeFree = [...new Set(activeUsers)].filter(x => !premiumUsers.includes(x));
}

Méthodes statiques d'Array

Source C# :

private void ProcessArrayOperations()
{
    var numbers = new[] { 5, 2, 8, 1, 9 };
    var items = new[] { "apple", "banana", "cherry" };

    // Sort array in place
    Array.Sort(numbers);
    // Result: [1, 2, 5, 8, 9]

    // Sort with custom comparison
    Array.Sort(items, (a, b) => b.Length - a.Length);
    // Result: ["banana", "cherry", "apple"]

    // Reverse array
    Array.Reverse(numbers);
    // Result: [9, 8, 5, 2, 1]

    // Find operations
    var firstEven = Array.Find(numbers, n => n % 2 == 0);
    var firstEvenIndex = Array.FindIndex(numbers, n => n % 2 == 0);
    var allEvens = Array.FindAll(numbers, n => n % 2 == 0);

    // Search operations
    var index = Array.IndexOf(numbers, 5);
    var lastIndex = Array.LastIndexOf(numbers, 5);

    // Check operations
    var hasEven = Array.Exists(numbers, n => n % 2 == 0);
    var allPositive = Array.TrueForAll(numbers, n => n > 0);

    // Clear and resize
    Array.Clear(numbers);
    Array.Resize(ref items, 5);  // Expand to 5 elements
}

Sortie TypeScript :

processArrayOperations() {
    const numbers = [5, 2, 8, 1, 9];
    const items = ["apple", "banana", "cherry"];

    // Sort
    numbers.sort();

    // Sort with comparison
    items.sort((a, b) => b.length - a.length);

    // Reverse
    numbers.reverse();

    // Find operations
    const firstEven = numbers.find(n => n % 2 == 0);
    const firstEvenIndex = numbers.findIndex(n => n % 2 == 0);
    const allEvens = numbers.filter(n => n % 2 == 0);

    // Search operations
    const index = numbers.indexOf(5);
    const lastIndex = numbers.lastIndexOf(5);

    // Check operations
    const hasEven = numbers.some(n => n % 2 == 0);
    const allPositive = numbers.every(n => n > 0);

    // Clear and resize
    numbers.splice(0);
    items.length = 5;
}

Filtrage par type LINQ (Cast et OfType)

Source C# :

private void FilterByType()
{
    // Mixed type collection
    object[] mixed = new object[] { 1, "hello", 2, "world", 3.14, true };

    // Cast<T>() - assumes all elements are of type T (passthrough in JS)
    var assumedStrings = mixed.Cast<string>();

    // OfType<T>() - filters to only elements of type T
    var onlyStrings = mixed.OfType<string>();
    // Result: ["hello", "world"]

    var onlyNumbers = mixed.OfType<int>();
    // Result: [1, 2]

    // Works with custom classes too
    var shapes = new object[] { new Circle(), new Square(), new Circle() };
    var circles = shapes.OfType<Circle>();
    // Result: [Circle, Circle]

    // Primitive type filtering
    var primitives = new object[] { 1, "text", 2.5, true, null };
    var strings = primitives.OfType<string>();  // ["text"]
    var numbers = primitives.OfType<double>();  // [1, 2.5]
    var booleans = primitives.OfType<bool>();   // [true]
}

Sortie TypeScript :

filterByType() {
    // Mixed type collection
    const mixed = [1, "hello", 2, "world", 3.14, true];

    // Cast - passthrough (JS is dynamically typed)
    const assumedStrings = mixed;

    // OfType - filter by typeof for primitives
    const onlyStrings = mixed.filter(x => typeof x === 'string');
    // Result: ["hello", "world"]

    const onlyNumbers = mixed.filter(x => typeof x === 'number');
    // Result: [1, 2, 3.14]

    // OfType - filter by instanceof for objects
    const shapes = [new Circle(), new Square(), new Circle()];
    const circles = shapes.filter(x => x instanceof Circle);
    // Result: [Circle, Circle]

    // Primitive filtering
    const primitives = [1, "text", 2.5, true, null];
    const strings = primitives.filter(x => typeof x === 'string');  // ["text"]
    const numbers = primitives.filter(x => typeof x === 'number');  // [1, 2.5]
    const booleans = primitives.filter(x => typeof x === 'boolean'); // [true]
}

Opérateurs C# modernes

Source C# :

private void DemonstrateModernOperators()
{
    // Null-coalescing assignment (??=)
    string? cachedData = null;
    cachedData ??= LoadDataFromDatabase();  // Only loads if null
    cachedData ??= "Default";               // Won't execute, already assigned

    // Property null-coalescing assignment
    if (user.Settings ??= new Settings())
    {
        Console.WriteLine("Created new settings");
    }

    // nameof operator (useful for property binding, validation)
    var propertyName = nameof(user.Email);
    Console.WriteLine($"Validating {propertyName}");  // "Validating Email"

    var methodName = nameof(ProcessOrder);
    LogAction(methodName);  // "ProcessOrder"

    // default keyword - type-safe default values
    int count = default(int);           // 0
    string? text = default(string);     // null
    bool flag = default(bool);          // false
    DateTime date = default(DateTime);  // 1/1/0001 12:00:00 AM

    // default literal (contextual)
    int number = default;               // 0 (inferred from type)
    ProcessData(default);               // passes default value for parameter type
}

private void ProcessData(int value = default)
{
    // value defaults to 0 if not provided
}

Sortie TypeScript :

demonstrateModernOperators() {
    // Null-coalescing assignment
    let cachedData = null;
    cachedData ?? (cachedData = this.loadDataFromDatabase());
    cachedData ?? (cachedData = 'Default');

    // Property assignment
    if (this.user.settings ?? (this.user.settings = new Settings())) {
        console.log('Created new settings');
    }

    // nameof operator
    const propertyName = 'Email';
    console.log(`Validating ${propertyName}`);

    const methodName = 'ProcessOrder';
    this.logAction(methodName);

    // default keyword
    let count = 0;
    let text = null;
    let flag = false;
    let date = $eq.time.dateTime.minValue();

    // default literal
    let number = 0;
    this.processData(0);
}

processData(value = 0) {
    // value defaults to 0
}

Méthodes de chaîne, exemples supplémentaires

Source C# :

private void StringManipulation()
{
    var text = "  Hello World  ";

    // Trimming
    var trimmed = text.Trim();              // "Hello World"
    var leftTrim = text.TrimStart();        // "Hello World  "
    var rightTrim = text.TrimEnd();         // "  Hello World"

    // Case conversion
    var upper = text.ToUpper();             // "  HELLO WORLD  "
    var lower = text.ToLower();             // "  hello world  "
    var upperInv = text.ToUpperInvariant(); // "  HELLO WORLD  "
    var lowerInv = text.ToLowerInvariant(); // "  hello world  "

    // Chaining methods
    var clean = text.Trim().ToLower().Replace("world", "everyone");
    // Result: "hello everyone"
}

Sortie TypeScript :

stringManipulation() {
    const text = "  Hello World  ";

    // Trimming
    const trimmed = $eq.text.trim(text);
    const leftTrim = $eq.text.trimStart(text);
    const rightTrim = $eq.text.trimEnd(text);

    // Case conversion
    const upper = text.toUpperCase();
    const lower = text.toLowerCase();
    const upperInv = text.toUpperCase();
    const lowerInv = text.toLowerCase();

    // Chaining
    const clean = $eq.text.trim(text).toLowerCase().replaceAll("world", "everyone");
}

Le rognage passe par le runtime parce que le trim de JavaScript est un ensemble différent de celui de .NET : il laisse U+0085 NEXT LINE, que .NET rogne, et prend U+FEFF, que .NET garde. char.IsWhiteSpace, string.IsNullOrWhiteSpace et un Split() nu lisent la même liste, donc un texte se rogne, se découpe et se tokenise pareil des deux côtés (Depuis 0.2.0-preview.58).

Clone this wiki locally