Skip to content

Compiler es

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

El compilador (CSharpToJs)

🌐 Esta página en: English · Português · Español · Français · Deutsch

El compilador es el componente central que hace posible la magia de eQuantic.UI. Transforma la semántica de C# en código TypeScript eficiente y legible.

La corrección se sostiene con una suite diferencial de conformidad: el mismo C# se evalúa en .NET y en el Bun incrustado, y las dos respuestas tienen que coincidir. Cualquier cosa que el compilador no pueda traducir con fidelidad es un error de compilación con una ubicación (los diagnósticos EQ2xxx), nunca una incorrección silenciosa.

🛡️ Fronteras (servidor frente a cliente)

Para mantener honesto el código del navegador, el compilador impone fronteras estrictas, inspiradas en Next.js (la división servidor/cliente) y Flutter (las restricciones), y las valida antes de emitir JS:

  • Componentes de cliente (StatefulComponent / StatelessComponent): lógica de UI, gestión de estado, System.Linq, tipos básicos (string, int, DateTime).
  • Prohibido en el cliente: System.IO, System.Net.Http directo, un .Wait() que bloquea. File.ReadAllText() en el cuerpo de un componente es un error de compilación.
  • El puente: la obtención de datos pasa por métodos anotados con [ServerAction] (al estilo RPC). Ver Seguridad y Server Actions.

🛠️ Componentes del compilador

1. TypeScriptEmitter

El TypeScriptEmitter es el punto de entrada para generar archivos .ts. Organiza los imports, define las clases y usa el CSharpToJsConverter para convertir los cuerpos de los métodos.

2. CSharpToJsConverter

Despacha todo nodo de Roslyn a una estrategia, una por construcción (BinaryExpressionStrategy, IfStatementStrategy, …), y devuelve IR, no texto. Las sentencias siempre construyen un JsStatement. Una estrategia de expresión que ya cruzó construye un JsExpr (IExpressionIrStrategy); una que todavía devuelve texto se empalma como un nodo opaco, idéntico byte a byte a lo que siempre produjo. Esa frontera es lo que deja a la migración avanzar una estrategia a la vez, y tests/eQuantic.UI.Compiler.Tests/Coverage/ir-migration.baseline.txt es la lista de estrategias de texto: solo se encoge, y una estrategia nueva nace en el IR.

3. SourceMapGenerator y SourceMapComposer

SourceMapGenerator escribe un mapa V3 estándar (VLQ en Base64) desde cada módulo de TypeScript que eqc emite de vuelta a las líneas .cs de las que vino, nombrando todo archivo del que el módulo se escribió (una clase que toma el default de una interfaz lleva ese default al archivo de la interfaz), cada uno dentro del proyecto, y con el C# mismo dentro solo cuando el mapa es full. Bun entonces mapea el JavaScript a ese TypeScript, y SourceMapComposer compone los dos, en C# y sin ningún paquete de npm, en UN mapa desde el JavaScript que el navegador ejecuta hasta el C#, conservando el debugId de bun. Los dos mapas pasan por un solo escritor de mapas, y ese escritor, como todo archivo JSON que la compilación escribe, por un solo escritor de JSON cuyo escape es el de System.Text.Json, así que un archivo de C# que contiene un carácter de control todavía tiene mapa. Lo que lleva un mapa (EQuanticSourceMaps: full en Debug, none en lo demás, external a pedido) está en Depuración.

4. Primero los símbolos, los nombres solo donde es honesto

El conversor pregunta al modelo semántico antes de adivinar. Un miembro, una variable local, un parámetro, un estático: cada uno emite desde su símbolo (this.name, Counter.name, un nombre desnudo), y Console.WriteLine es console.log porque el símbolo dice System.Console. Las heurísticas de nombre (un guion bajo inicial, una propiedad capitalizada) son legales solo donde al modelo no se le puede preguntar: un fragmento sin modelo, un nodo que una estrategia reescribió. Bajo un modelo autoritativo, un nombre del árbol que no enlaza es un error de compilación (EQ2006), nunca una traducción adivinada.

5. El IR y sus escritores

Desde 0.2.0-preview.36

CodeGen/Ir/ es un árbol pequeño con un escritor por nivel, JsExpr → JsStatement → JsClassMember → JsClass → JsModule, y los escritores son dueños de todo lo que una estrategia nunca debe escribir a mano:

  • Los paréntesis vienen de la precedencia y la asociatividad, nunca de una plantilla. Antes de esto, f ?? g && g se entregaba literal: C# no necesita paréntesis ahí, JavaScript rechaza la mezcla desnuda, y el bundle entero no se podía parsear.
  • Una sola evaluación: un JsTemplate nombra lo que computa ({0} === {0}.normalize()) y el escritor enlaza exactamente una vez una parte usada más de una vez: un nombre simple o un literal se inlinea, una lectura de miembro no (un getter puede contar). Una parte dentro de una función que la plantilla define también se enlaza, incluido un nombre simple, ya que esa función corre una vez por elemento: solo un literal, this y una lambda escrita en el sitio se quedan dentro, y también una parte que lee un nombre que la función declara.
  • La disposición: una sentencia por línea, bloques indentados (JsLayout.Pretty); Compact reproduce el antiguo mundo de cadenas byte a byte, que es cómo se prueba cada paso de la migración. Una clase tiene una sola regla de disposición, una línea en blanco antes de un miembro con cuerpo, los campos contiguos, y un módulo son sus imports como records JsImport, una línea en blanco, su cuerpo.
  • El emisor (TypeScriptEmitter) decide qué contiene un módulo y entrega nodos al constructor; no ensambla texto.

Dos redes sostienen todo eso: las fijaciones de componente (el módulo generado de todo componente compartido, byte a byte: un cambio de disposición tiene que ser solo de espacios contra ellas) y la suite de conformidad, que ejecuta toda forma traducida en los dos lados y compara las respuestas. Los gemelos generados bajo src/eQuantic.UI.Runtime/src/shared/components están fijados, con tipos comprobados y probados, no pasados por linter: el código generado responde a su escritor, no a una guía de estilo.

🔄 Estrategias soportadas

Actualmente el compilador soporta un amplio rango de construcciones de C#:

  • Expresiones: aritméticas, lógicas, ternarias, interpolación de cadenas, fusión de nulos (??), acceso condicional (?., ?[]). Un argumento que espera detrás de ?. se espera donde está escrito, solo cuando el receptor no es nulo (Desde 0.2.0-preview.60), sea cual sea el receptor: una llamada, un elemento o una propiedad delante de ?. se lee una vez a una variable del método en el que está escrita (Desde 0.2.0-preview.61; hasta entonces tal receptor rompía la compilación con EQ1004). Una lectura condicional de nulo responde null donde su valor se usa, como lo hace C#, nunca el undefined de JavaScript (Desde 0.2.0-preview.61). Un grupo de métodos es el delegado que hace C#: su receptor se lee una vez, cuando el delegado se hace, base.M llama al método de la base sobre este objeto, y el grupo de un método de extensión se enlaza en la clase que lo declara, donde va su llamada. Un grupo sobre una extensión que nada en el bundle declara, como list.Any, rompe la compilación con EQ2004 (Desde 0.2.0-preview.61)
  • Flujo de control: if, switch, for, foreach, while, do-while, break, continue, throw
  • Patrones modernos: soporte completo de patrones recursivos, de propiedad, posicionales, relacionales y lógicos (C# 9.0 a 12.0)
  • Gestión de recursos: soporte de sentencias using y declaraciones using var
  • Excepciones: soporte completo de try-catch-finally y de sentencias throw (Exception → Error)
  • Índices y rangos: soporte del operador de índice desde el final (array[^1] → array[array.length - 1])
  • Métodos de cadena: métodos de instancia (Split, Replace, StartsWith, EndsWith, Contains, Substring, IndexOf, LastIndexOf, PadLeft, PadRight, Trim, TrimStart, TrimEnd, ToUpper, ToLower, ToUpperInvariant, ToLowerInvariant, Insert, Remove, ToCharArray) y métodos estáticos (IsNullOrEmpty, IsNullOrWhiteSpace, Join, Concat, Compare, Equals, Format). Un método al que se le da un StringComparison responde por él, por el runtime, y una búsqueda por una comparación de cultura rompe la compilación (EQ1004): el navegador no tiene búsqueda consciente de la cultura (Desde 0.2.0-preview.60)
  • Métodos numéricos: Parse y TryParse en todo tipo numérico (int, uint, long, ulong, short, ushort, byte, sbyte, float, double, decimal), leídos por el runtime con la gramática de .NET
  • Métodos 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
  • Caras de List: un elemento leído o escrito por IList<T>, IReadOnlyList<T> o IList → $eq.collections.item(list, i) y $eq.collections.setItem(list, i, value), su Count → $eq.collections.count(list), y el Add y el Clear propios de ICollection<T> → $eq.collections.add(collection, item) y $eq.collections.clear(collection). Cada uno responde por la colección que la cara contenga cuando corre: un arreglo por su subíndice, el conjunto, la lista enlazada o el diccionario del runtime por sus propios miembros, y un tipo propio tuyo por su indexador, su Count, su Add y su Clear (Desde 0.2.0-preview.61)
  • Métodos estáticos de Array: soporte completo de los métodos estáticos de Array
    • Array.Sort(array) → array.sort() - Ordena el arreglo en el sitio
    • Array.Sort(array, comparison) → array.sort(comparison) - Ordena con un comparador propio
    • Array.Reverse(array) → array.reverse() - Invierte el arreglo en el sitio
    • Array.Find(array, predicate) → array.find(predicate) - Encuentra el primer elemento que coincide
    • Array.FindIndex(array, predicate) → array.findIndex(predicate) - Encuentra el índice de la primera coincidencia
    • Array.FindAll(array, predicate) → array.filter(predicate) - Encuentra todos los elementos que coinciden
    • Array.IndexOf(array, value) → array.indexOf(value) - Encuentra el índice de un valor
    • Array.LastIndexOf(array, value) → array.lastIndexOf(value) - Encuentra el último índice de un valor
    • Array.Exists(array, predicate) → array.some(predicate) - Comprueba si algún elemento coincide
    • Array.TrueForAll(array, predicate) → array.every(predicate) - Comprueba si todos los elementos coinciden
    • Array.Clear(array) → array.splice(0) - Quita todos los elementos
    • Array.Resize(ref array, size) → array.length = size - Redimensiona el arreglo
  • Métodos de Enum: un enum no tiene objeto propio en el navegador, así que los estáticos de Enum llaman a las funciones de enum del runtime, $eq.enums, con la forma del enum escrita en la llamada: sus nombres, los nombres camelCase que el navegador conserva, sus valores y si es un enum [Flags] (Desde 0.2.0-preview.60; nombraban un objeto que ningún módulo declara, y lanzaban)
    • Enum.Parse<T>(text) y Enum.Parse(typeof(T), text) → $eq.enums.parse(text, shape): un nombre, nombres unidos por comas o un número, el caso conservado salvo que ignoreCase sea true, y un error donde .NET lanza uno
    • Enum.TryParse<T>(text, out var result) → true con el valor, o false con el valor por defecto del enum en result, y null para la sobrecarga que toma un Type
    • Enum.GetName(value) → el nombre del miembro con ese valor, o null donde ninguno lo tiene
    • Enum.GetNames<T>() y Enum.GetValues<T>() → los nombres y los valores, en el orden de los valores
    • Enum.IsDefined(typeof(T), value) → si algún miembro tiene ese valor, dado como el enum, un número, un nombre declarado, o un object que contenga cualquiera de ellos
    • value.ToString(), una interpolación y una concatenación escriben el nombre del miembro, las banderas puestas de una combinación de flags (Read, Write), los dígitos de un valor que ningún miembro nombra, y nada para un enum nullable nulo. ToString("D"), "X", "F" y "G", y el formato de una interpolación, escriben lo que escribe .NET, y una alineación rellena ese texto
    • Un cast desde un object que contiene el enum lo conserva ((Status)Enum.Parse(typeof(Status), text)), un cast entre dos enums pasa por sus valores, y un valor que ningún miembro nombra se guarda como su número
  • Métodos de Dictionary: todo Dictionary, IDictionary e IReadOnlyDictionary, y los ordenados, es una clase del runtime bajo $eq.collections.*, preguntada con has/get (Desde 0.2.0-preview.60)
    • new Dictionary<K, V>() → $eq.collections.dictionary(), o $eq.collections.dictionary(null, true) donde el tipo de la clave compara por valor (un record, un struct, una tupla, un decimal, una fecha, una clase que sobrescribe Equals); una copia lo siembra con su fuente, y un inicializador con pares [key, value]
    • dict[key] → $eq.mapGet(dict, key) - lanza para una clave que falta, como lo hace .NET; dict[key] = value → $eq.mapSet(dict, key, value), que responde el valor escrito
    • ContainsKey(key) → dict.has(key) - nunca responde por un miembro del prototipo
    • TryGetValue(key, out var value) → (dict.has(key) ? ((value = dict.get(key)), true) : ((value = 0), false)) - un fallo escribe default(TValue) (0 aquí, para un int)
    • GetValueOrDefault(key[, defaultValue]) → (dict.has(key) ? dict.get(key) : 0) - un valor por defecto explícito se evalúa una vez, se necesite o no
    • Un receptor o una clave leídos por un miembro o una llamada se enlazan una vez: this.cache.TryGetValue(Next(), out var v) → (($0, $1) => …)(this.cache, next())
    • Add(key, value) → dict.set(key, value) - NO lanza para una clave ya presente, donde el de .NET sí (#440)
    • Remove(key) → dict.delete(key), y Remove(key, out var value) mueve el valor al out
    • TryAdd(key, value) → dict.tryAdd(key, value), ContainsValue(value) → dict.containsValue(value)
    • Clear() → dict.clear()
    • Keys / Values (propiedades) → dict.keys() / dict.values(), arreglos en el orden del diccionario; Count, Keys.Count y Values.Count → dict.size, y Keys.Contains(key) → dict.has(key)
  • LINQ: conversión directa de los métodos de LINQ a sus equivalentes de JS:
    • Proyección: Select → map, SelectMany → flatMap
    • Filtrado: Where → filter, Distinct → [...new Set()]
    • Ordenación: OrderBy/OrderByDescending → sort, Reverse → [...arr].reverse()
    • Partición: Skip → slice(n), Take → slice(0, n)
    • Elemento: First/FirstOrDefault → find/[0], Last/LastOrDefault → arr[arr.length-1], Single/SingleOrDefault → find/[0]; con un valor por defecto, FirstOrDefault, LastOrDefault y SingleOrDefault → $eq.linq.firstOrDefault, $eq.linq.lastOrDefault y $eq.linq.singleOrDefault, que lo responden donde nada coincide (Desde 0.2.0-preview.62)
    • Cuantificadores: Any → some/length > 0, All → every, Contains → includes
    • Agregación: Count → length/filter().length, Sum → reduce(($a, $b) => $a + $b, 0), Average → $eq.linq.average, que computa la media en el tipo del elemento, un long, un float, un decimal o un nullable (Desde 0.2.0-preview.62), Min → Math.min(...), Max → Math.max(...)
    • Operaciones de conjunto:
      • Concat(other) → [...source, ...other] - Concatena dos secuencias
      • Union(other) → [...new Set([...source, ...other])] - Elementos únicos de las dos secuencias
      • Intersect(other) → [...new Set(source)].filter(($x) => other.includes($x)) - Elementos comunes
      • Except(other) → [...new Set(source)].filter(($x) => !other.includes($x)) - Elementos en la fuente pero no en la otra
      • other, como todo argumento de una llamada de LINQ que no es una lambda escrita en el sitio, se evalúa una vez, en el orden de C#, antes de que corra el lowering: corría una vez por elemento (Desde 0.2.0-preview.61)
    • Pares: Zip(second) y Zip(second, third) → $eq.linq.zipTuples, que responde los pares y las ternas que responde .NET (Desde 0.2.0-preview.62)
    • Igualdad: SequenceEqual → $eq.linq.sequenceEqual, que compara por la igualdad por defecto del tipo del elemento (Desde 0.2.0-preview.62)
    • Comparadores: un comparador entregado a ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet o SequenceEqual que pide el por defecto (EqualityComparer<T>.Default, null, StringComparer.Ordinal) se descarta, ya que el lowering ya encuentra sus claves así, y cualquier otro rompe la compilación con EQ2007 (Desde 0.2.0-preview.61)
    • Filtrado por tipo:
      • Cast<T>() → paso directo (JavaScript es de tipado dinámico)
      • OfType<T>() → filter(($x) => typeof $x === 'type') para primitivos, filter(($x) => $x instanceof Type) para objetos
  • Async/Await: mapeo de Task a Promise y soporte nativo de await.
  • Operadores modernos de C#: soporte de operadores y palabras clave modernas de C#
    • Asignación con fusión de nulos: x ??= value → x ?? (x = value) - Asigna solo si es nulo o undefined
    • Operador nameof: nameof(variable) → 'variable' - Obtiene el nombre como cadena en tiempo de compilación. Es el nombre, no su escritura: nameof(@class) → 'class' (Desde 0.2.0-preview.60)
    • Palabra clave default: default(T) y el literal default son el valor por defecto del tipo, siendo el tipo del literal aquel al que C# lo convierte: default(int) → 0, default(long) → 0n, default(int?) → null, int x = default → 0 (Desde 0.2.0-preview.58)

Lo que C# te da gratis, y JavaScript no

Dos valores por defecto son implícitos en C# y ausentes en JavaScript. Los dos se emitían como nada por un tiempo, y los dos fallan TARDE, no en tiempo de compilación, y no donde está la causa.

Un tipo de valor sin asignar es CERO

Desde 0.2.0-preview.22

Un campo de un tipo de valor es cero haya escrito alguien = 0 o no. En el cliente era undefined, y los dos no son el mismo valor. Las lecturas sobreviven por suerte mientras son PRUEBAS (undefined > 0 es false, que es lo que habría dicho 0), y entonces la primera ARITMÉTICA lo vuelve NaN. Math.max(width, undefined) llega a la hoja de estilos como width:NaNpx, una regla que el parser de CSS descarta entera: la clase se computa, se hashea, se emite, se pone en el elemento, y no hace nada.

Aparece solo en páginas RENDERIZADAS en el cliente, nunca en SSR ni en una carga directa, porque el servidor computa la misma propiedad en C# donde fue 0 todo el tiempo. Así que los dos destinos discrepan sobre un campo y la página que lo prueba es la que nadie recarga.

Todo tipo de valor no nullable lleva ahora su valor por defecto (0, false, el miembro cero de un enum). Los nullables no, porque ahí null ES la respuesta de C#, e inventar un cero sería la misma divergencia apuntando al otro lado.

Un parámetro de constructor primario es ESTADO de instancia

Desde 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 modela la captura como un IParameterSymbol, así que todo lugar que pregunta «¿es esto un parámetro?» responde que sí sobre algo que se comporta como un campo. Emitido desnudo compila, la página se renderiza, y el ReferenceError llega cuando el callback por fin se dispara, saliendo a la superficie desde dentro del reconciliador como un TypeError sobre algo completamente distinto.

Los modelos simples también cruzan

Un componente no es el único C# que una página necesita. El modelo de documento detrás de un editor, una pequeña máquina de estados, un parser: ninguno de ellos es un componente, y todos tienen que correr en los dos destinos. Transpilan igual, como módulos propios, y las reglas de abajo son lo que hace la emisión COMPROBADA en vez de meramente presente.

Los rangos son tajadas

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 última forma es la que JavaScript no puede decir directamente: ^0 significa el FINAL, mientras que slice(0, -0) es slice(0, 0), que está vacío. Cualquier cosa que no sea un literal positivo después de ^ se resuelve por tanto contra la longitud como lo hace Index.GetOffset. Un Range guardado como VALOR se reporta (EQ2004): nada del otro lado recibe uno, e indexar en el punto de uso es para lo que sirve.

Un rango sobre un tipo propio tuyo con un Length (o un Count) y un Slice(int start, int length) no es la tajada de un arreglo: C# lo baja a ese Slice, que toma un inicio y una LONGITUD donde el slice de JavaScript toma un final, y lo mismo hace el gemelo (Desde 0.2.0-preview.61):

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

El receptor se lee una vez, luego los extremos en el orden en que están escritos, luego el Length o el Count, leído solo donde un extremo cuenta desde el final o el final se deja abierto, como los lee .NET. Un rango entregado a un indexador que toma el Range mismo le entregaría un valor Range, y rompe la compilación (EQ2004) por la misma razón que lo hace uno guardado.

out y ref

JavaScript no tiene ninguno. Un método que los declara devuelve un OBJETO (su propio valor bajo $, cada out y ref bajo su nombre) y su cuerpo se mueve dentro de un cierre para que todo return en él siga significando lo que significaba. El sitio de llamada lo desenvuelve con una flecha, lo cual funciona en cualquier posición de expresión, incluso dentro de 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 deja la lista de parámetros de JS (no se pasa HACIA DENTRO); ref se queda, porque se lee antes de escribirse. out _ no asigna nada.

Las variables de expresión se declaran donde C# las declara

El enlace de un patrón (x is T t), un out var y el elemento de una deconstrucción ((var c, var d) = …) se asignan dentro de la expresión que los declara, así que la sentencia que contiene esa expresión los declara, con el ámbito que Roslyn les da (Desde 0.2.0-preview.59):

  • una sentencia de expresión, un if, un return, un throw, un yield return, una declaración, la expresión gobernante de un switch y un lock los declaran delante de sí mismos, donde el código después de ellos puede leerlos (if (!int.TryParse(s, out var n)) return; y luego n). Puestos directamente en una sección de switch, los dejan al switch, que los declara una vez para todo su bloque, como C# los enmarca, así que otra sección puede asignarlos;
  • un while, un do y un for los declaran en la cabeza propia del bucle, for (let n; …), que JavaScript copia para cada iteración, como .NET le da a la condición de un bucle una variable fresca: un cierre hecho en una iteración conserva el valor de esa iteración;
  • un foreach y un using los declaran en un bloque alrededor de la sentencia, que es su ámbito en C#;
  • una lambda, una función local, la cláusula de una consulta y la rama de una expresión switch declaran los suyos, una vez por cada llamada;
  • el inicializador de un campo o de una propiedad los declara en una flecha propia.

Un nombre que JavaScript rechaza, y que el escape textual de C# permite (@class, @new, @default), se vuelve un nombre legal en su declaración y en toda referencia, de cualquier manera que se haya enlazado: class$, con un $ que ningún nombre de C# puede contener. Una etiqueta y un parámetro de tipo de un método escritos así toman el mismo nombre, y un miembro o una clave de with es el nombre mismo: r.class, { class: 5 } (Desde 0.2.0-preview.61).

Una variable local nombrada como un global que el código emitido lee (crypto, undefined, Math, Number, console) toma el mismo $, así que el global se queda visible para el código a su lado, y todo nombre que el código emitido declara para sí mismo empieza con un $, así que nunca se encuentra con una local que el código a su alrededor lee (Desde 0.2.0-preview.61).

Colecciones: la capacidad no es el contenido

new List<T>(x) significa dos cosas opuestas según qué sea x, y solo el constructor resuelto puede decir cuál: new List<string>(other.Count) es una lista vacía dimensionada por adelantado, new List<string>(other) es una copia. La primera emite un arreglo vacío, la segunda [...other]. Con un inicializador de colección, la lista es lo que su constructor puso en ella, y luego cada elemento en orden: new List<string>(other) { "x" } es [...other, "x"] y new List<string>(8) { "x" } es ["x"], escrito o tipado por destino. Una capacidad que no es una constante que la lista toma se evalúa igual, antes de los elementos, y una negativa se rechaza como la rechaza el constructor de List<T>: new List<string>(Size()) { "x" } es ($eq.collections.listCapacity(this.size()), ["x"]) (Desde 0.2.0-preview.61).

La aritmética de char computa en unidades de código

Un char de C# en + - * / % promueve a int y computa en la unidad de código, mientras que un char transpilado es una cadena de longitud 1. Cuando el tipo del RESULTADO es numérico, los operandos char bajan a unidades de código (los literales constantes se doblan al número; las expresiones leen charCodeAt(0)), así que text[i] - '0' es el dígito y 'A' + col es un número, exactamente como en .NET. (char)numeric baja a String.fromCharCode. char + string se queda concatenación: su tipo de resultado es string, así que la rama numérica nunca lo ve.

Un valor del servidor cruza una frontera TIPADA

JavaScript no tiene decimal ni entero de 64 bits, así que el cable envía esos como cadenas, un long como "9007199254740993", un decimal como "0.1", la familia de fechas como texto ISO. El compilador conoce el tipo de C# de todo campo de estado y de todo retorno de Server Action, así que escribe ese conocimiento en el gemelo como un mapa estático $hydration, y el runtime coacciona UNA VEZ, donde el valor llega, en vez de que todo sitio de uso coaccione a la defensiva. Un record nombra su propio mapa, una lista es [spec], los valores de un diccionario son { dict: spec }, y una tupla es posicional ({ tuple: [...] }) porque cruza como un arreglo. No se emite nada donde la hidratación sería la identidad, que es la mayoría de los campos.

Un valor por defecto lo decide el TIPO, y nunca es nulo para un tipo de valor

new int[0].SingleOrDefault() es 0 en .NET, no null, y lo mismo vale para FirstOrDefault, LastOrDefault, ElementAtOrDefault y DefaultIfEmpty. Un campo declarado sin inicializador toma el mismo valor por defecto: un int es 0, un bool false, un long 0n, y un ENUM su miembro de valor cero, que es una cadena con el NOMBRE del miembro en este lado, así que un campo dejado sin asignar no renderizaría nada en absoluto. Una sola tabla responde por los dos, por símbolo, así que un tipo alcanzado por un alias (using Amount = decimal;) no es una respuesta distinta del tipo mismo.

La misma tabla llena un arreglo dimensionado, new T[n], y responde default(T) y el literal default, cuyo tipo es aquel al que C# lo convierte: el de una variable local, el de un parámetro, el de la otra rama de un condicional. Un elemento struct es una instancia cero propia, ya que fill compartiría un objeto por toda ranura, y los gemelos de tiempo e identidad (DateTime, TimeSpan, DateOnly, TimeOnly, DateTimeOffset, Guid) empiezan en su MinValue, Zero o Empty. Un struct cuyo gemelo escrito a mano no puede construir su cero conserva el valor por defecto propio del gemelo en un literal default: undefined, que el constructor del gemelo vuelve su valor por defecto, donde null lo saltaría. Desde 0.2.0-preview.58.

Un segundo OrderBy reinicia la ordenación

xs.OrderBy(a).OrderBy(b) ordena por b. La ordenación anterior no se queda como clave primaria: sobrevive solo como el desempate que le da una ordenación estable, así que es un resultado distinto de xs.OrderBy(a).ThenBy(b) siempre que b tenga empates que a rompería. Las dos cruzan con fidelidad: un OrderBy encadenado ordena su fuente primero y ordena eso, y ThenBy compone su clave en la misma comparación.

Un medio redondea al vecino PAR, como .NET

Math.round(32.5) es 33 en JavaScript y 32 en .NET, que redondea un medio al par. En todos los lugares donde el runtime espeja un MathF.Round usa la regla de .NET, así que un valor computado en el navegador es el valor que computó el servidor: esto es lo que mantiene la altura de línea de una escala tipográfica, y cualquier maquetación derivada de una, idéntica a través de la hidratación en vez de una fracción desviada.

Es la aritmética de .NET, no una aproximación de ella. El punto medio es EXACTO: Math.Round(0.5015, 3) es 0,501, porque 0.5015 * 1000 es 501,49999999999994: un valor cerca de un medio no es un medio. Todo modo de MidpointRounding se honra, una cuenta de dígitos pasada de 15 (6 para un float) lanza como lo hace la de .NET, y MathF.Round(x, digits) escala y divide en precisión simple.

La superficie de LINQ es una tabla, y la BCL numérica es otra

La mayoría de los operadores de LINQ son una forma cada uno (Where es filter, Aggregate es reduce con sus argumentos intercambiados), así que viven como entradas en una tabla indexada por operador y cuenta de argumentos, con una sola puerta, y con el escritor del IR puntuando y enlazando cualquier receptor usado dos veces. Un operador que tiene que RAZONAR conserva una estrategia propia: la familia OrDefault lee el tipo del elemento, Sum elige una semilla por él, Cast y OfType prueban tipos, Contains elige entre igualdad de identidad y estructural. La misma forma vale para la BCL numérica, donde la superficie moderna (Double.AcosPi y su familia) es una tabla de plantillas en vez de un método cada una.

Donde esas tablas no tienen entrada, la llamada es un ERROR de compilación que nombra el miembro, nunca una adivinanza: un nombre emitido sin nada detrás es un ReferenceError al cargar, y una página que nunca se renderiza.

Los enteros de ancho fijo se asientan por su tipo

Un byte de C# pasado de 255 da la vuelta; un número de JavaScript sigue contando. El compilador lee el tipo del RESULTADO de todo + - * << (y de ++, -- y las formas compuestas) y asienta el valor donde C# lo haría: byte, sbyte, short, ushort y uint SIEMPRE dan la vuelta (& 0xFF, << 16 >> 16, >>> 0: los valores empaquetados y los hashes dependen de ello, así que h *= 16777619 en un uint es el paso FNV que es en .NET, por Math.imul). int y long dan la vuelta solo donde escribiste unchecked (unchecked(a * a) es 0 para 65536; un long da la vuelta por BigInt.asIntN), porque de otro modo todo i + 1 simple en una UI llevaría un | 0 por un desbordamiento que es un defecto en cualquier otro lugar, así que un int.MaxValue + 1 simple conserva la cuenta del double, un límite documentado. Un contexto checked (el bloque, la expresión, o el ajuste de todo el proyecto, leído del IsChecked del árbol enlazado y no de la sintaxis) lanza un desbordamiento exactamente donde C# lo lanza. Un float es un simple donde sea que se PRODUZCA, todo + - * /, todo incremento y asignación compuesta sobre un destino float (uno float? todavía no se redondea), un int pasado de 2^24 en su camino hacia dentro, una constante float, un valor hidratado del servidor, porque RyuJIT redondea cada operación de float, y así a*x - b*x responde el último bit del servidor también en el navegador. Se imprime como el decimal más corto que se vuelve a leer como el mismo simple, así que 0.1f + 0.2f es "0.3" en los dos lados. char++ avanza el carácter; un enum en aritmética (day + 1, a.CompareTo(b)) computa sobre el valor detrás del nombre del miembro.

Las conversiones implícitas las asienta el árbol enlazado

La sintaxis nunca muestra una conversión implícita (int i = c con un char, Twice(c), a[c], long l = n) y una regla escrita contra la sintaxis cubre solo las formas que su autor recordó. El compilador las lee del árbol enlazado de Roslyn en su lugar: después de que toda expresión se traduce, la conversión en la que el árbol enlazado la envuelve se aplica (ValueFlow), en todo sitio en el que C# la aplica: inicializadores, argumentos, índices, retornos, comparaciones. Un char promovido a número se vuelve su unidad de código (una constante se dobla: 'A' + col es 65 + col); un int que fluye a un long se vuelve un BigInt (1 → 1n, que es además por lo que TimeSpan.FromSeconds(90) emite 90n: la sobrecarga de .NET 9 toma un long). Dos chars comparados se quedan caracteres: JavaScript ordena las cadenas de longitud 1 por las mismas unidades de código. Una conversión definida por el usuario cruza como una llamada al estático que lleva su gemelo, un tipo propio de una app como Money.fromInt(5), un tipo del vocabulario como IconGlyph.fromIcons('search') para Icon(Icons.Search), salvo que su gemelo tome el operando tal cual: SizeValue desde un número declara exactamente eso, así que Width = 120 se queda un 120 simple, y un tipo de fuera del SDK (Index) pasa su primitivo directo. Un valor en su camino hacia TEXTO se asienta igual: encajado en una concatenación, un operando de cadena de una, o un hueco de interpolación simple, se imprime como lo imprime C#, un nulo como nada, un bool como "True", un enum por el nombre de su miembro, s += flag incluido, que ninguna regla de sintaxis había visto.

Los operadores propios de un tipo cruzan

JavaScript no puede sobrecargar +, así que un record o un struct que declares con operadores lleva cada uno en su gemelo como un método estático, y todo sitio donde el árbol enlazado muestra un operador lo llama: a + b es Money.opAdd(a, b), -m es Money.opNegate(m) (el - unario y el binario se nombran por aridad, así que nunca chocan), m += other es m = Money.opAdd(m, other). Las conversiones también: implicit operator Money(int v) se vuelve Money.fromInt, llamado donde sea que C# convierta, una declaración, un argumento, el operando de un compuesto, y explicit operator int(Money m) se vuelve Money.toInt, llamado por el cast. Solo para tipos en tu fuente: una envoltura del framework como SizeValue o Index es su primitivo en este lado, y sus operadores pasan el valor directo.

El ELEMENTO de un foreach se convierte igual, uno a la vez: foreach (long l in ints) vuelve cada int un BigInt, foreach (int code in chars) cada char su unidad de código, foreach (Money m in ints) llama a la conversión del tipo: el bucle enlaza un $l crudo y declara l desde él. Una DECLARACIÓN using (using var r = …;) es dueña del resto de su bloque: lo que sigue corre dentro de un try cuyo finally libera r, después de que se toma un valor de retorno, cuando el cuerpo lanza, en orden inverso cuando varios comparten un bloque, y await using espera a disposeAsync.

Los diccionarios se enumeran como los de .NET

Un diccionario es la clase de diccionario del runtime, que conserva sus entradas por ranura como lo hace el Dictionary de .NET: se enumera en orden de inserción mientras nada se quita, y una entrada agregada después de un quitado toma la ranura de la quitada, la última liberada primero. foreach sobre uno (y new List<KeyValuePair<,>>(dict), que lo esparce) produce pares que se desestructuran como [key, value] Y responden .key/.value (las dos formas de consumo de C#), cada clave en su propio tipo, así que el siguiente key + 1 suma. Un diccionario ordenado produce los mismos pares, en orden de clave. Desde 0.2.0-preview.60.

Las anotaciones de elemento genérico ceden a la inferencia

new List<KeyValuePair<int,float>>(…) no puede anotar su let con el nombre desnudo de C# (KeyValuePair[] no nombra nada en TS); los elementos genéricos dejan la anotación a la inferencia.

Los estáticos se inicializan como un TIPO, al primer uso

C# inicializa los estáticos de un tipo juntos: cada uno empieza en su cero, luego los inicializadores corren en orden de declaración, luego el constructor estático, una vez, al primer uso. Un tipo cuyos estáticos pueden observarse unos a otros (un inicializador que no es una constante, o un constructor estático) los contiene todos en un solo objeto $slots que $init() construye la primera vez que uno de ellos se lee o se escribe, y cada estático es un par de accesores sobre su ranura, así que static int A = B + 1; encima de static int B = 2; lee el cero de B y responde 1, como lo hace .NET, y dos tipos cuyos estáticos se leen uno al otro ven el cero que ve .NET. El mismo mecanismo sirve a un record, un struct, una clase, una clase estática y un componente. Siendo perezoso, además sobrevive a los ciclos de import de la biblioteca compartida (sus módulos se importan unos a otros por un solo barril, y el que cargue primero ve la clase del otro como undefined), que es para lo que servía el getter perezoso por estático que reemplaza; ese getter inicializaba cada estático en su propia primera lectura, sin orden alguno. Un tipo cuyos inicializadores son constantes conserva campos simples, cada uno escrito como el VALOR al que C# lo dobla (static readonly int Max = Default * 2; encima de const int Default = 50; es 100, donde la expresión leía Default antes de que su declaración lo definiera), y un estático cuyo cero construye un gemelo se construye al primer uso también, nunca mientras su módulo se evalúa. Un tipo que declara un constructor estático además arranca la inicialización primero en su constructor de instancia y en todo método, accesor y operador estáticos, ya que C# corre ese constructor antes de la primera instancia y del primer uso de cualquier miembro estático. Los estáticos se inicializan en orden de declaración a través de campos, propiedades y eventos tipo campo; $slots existe antes de que nada corra, así que una lectura reentrante ve el cero que ve C#; el cuerpo del constructor estático corre en una función propia después de los inicializadores, así que un return en él lo termina solo; y un fallo se conserva, y todo uso del tipo entonces lanza una TypeInitializationException cuya excepción interna es la original, como lo hace .NET. Desde 0.2.0-preview.61.

Qué puede nombrar una ANOTACIÓN de tipo

El .ts emitido se comprueba con tipos (esa es la segunda de las dos capas), así que una firma nunca debe introducir un nombre que el módulo no puede resolver:

C# TypeScript
un enum string, ya que su representación en tiempo de ejecución es el nombre del miembro
una interfaz sin gemelo emitido any
IReadOnlyList<(char, char)> [string, string][]
Action<T>? ((t: T) => void) | null, entre paréntesis, o la unión se enlaza al retorno
char string
un nombre que nada puede verificar any, porque un tipo equivocado es peor que uno abierto

Los records llevan las mismas reglas, más sus campos estáticos y sus propiedades computadas (un record es un valor con COMPORTAMIENTO, no solo sus miembros posicionales).

La biblioteca ES su directorio

Tanto el conjunto transpilado como el barril de exportación del runtime se generan desde el directorio de fuentes, nunca desde un registro mantenido a mano, así que la biblioteca incrustada nunca puede apartarse del código del que se construye.

📝 Ejemplo de conversión

Fuente de C#:

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

Salida de TypeScript:

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

🎯 Ejemplos de funciones avanzadas

Operaciones con enums

Fuente de 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");
    }
}

Salida de TypeScript (shape representa la forma del enum, que eqc escribe en cada llamada: { 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');
    }
}

Operaciones con diccionarios

Fuente de 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();
}

Salida de 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();
}

Operaciones de conjunto de LINQ

Fuente de 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);
}

Salida de 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étodos estáticos de Array

Fuente de 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
}

Salida de 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;
}

Filtrado por tipo de LINQ (Cast y OfType)

Fuente de 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]
}

Salida de 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]
}

Operadores modernos de C#

Fuente de 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
}

Salida de 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étodos de cadena, ejemplos adicionales

Fuente de 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"
}

Salida de 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");
}

El recorte pasa por el runtime porque el trim de JavaScript es un conjunto distinto del de .NET: deja U+0085 NEXT LINE, que .NET recorta, y toma U+FEFF, que .NET conserva. char.IsWhiteSpace, string.IsNullOrWhiteSpace y un Split() desnudo leen la misma lista, así que un texto se recorta, se parte y se tokeniza igual en los dos lados (Desde 0.2.0-preview.58).

Clone this wiki locally