O compilador (CSharpToJs)O compilador é o componente central que torna possível a mágica do eQuantic.UI. Ele transforma a semântica do C# em código TypeScript eficiente e legível.
A correção é sustentada por uma suíte diferencial de conformidade: o mesmo C# é avaliado no .NET e no Bun embarcado, e as duas respostas têm que concordar. Qualquer coisa que o compilador não consiga traduzir fielmente é um erro de build com localização (os diagnósticos EQ2xxx), nunca um erro silencioso.
🛡️ Fronteiras (servidor vs cliente)
Para manter o código do browser honesto, o compilador impõe fronteiras estritas, inspiradas no Next.js (a divisão servidor/cliente) e no Flutter (restrições), e as valida _antes_ de emitir JS:
•
Componentes cliente (StatefulComponent / StatelessComponent): lógica de interface, gestão de estado, System.Linq, tipos básicos (string, int, DateTime).
•
Proibido no cliente: System.IO, System.Net.Http direto, .Wait() bloqueante. Um File.ReadAllText() no corpo de um componente é erro de build.
🛠️ Componentes do compilador
O TypeScriptEmitter é o ponto de entrada para gerar os arquivos .ts. Ele organiza os imports, define as classes, e usa o CSharpToJsConverter para converter os corpos dos métodos.
Despacha cada nó do Roslyn para uma strategy — uma por construto (BinaryExpressionStrategy, IfStatementStrategy, …) — e devolve IR, não texto. Statements sempre constroem um JsStatement. Uma strategy de expressão que já atravessou constrói um JsExpr (IExpressionIrStrategy); uma que ainda devolve texto entra como nó opaco, byte-idêntico ao que sempre produziu. Essa fronteira é o que deixa a migração andar uma strategy por vez, e tests/eQuantic.UI.Compiler.Tests/Coverage/ir-migration.baseline.txt é a lista das strategies em texto — só encolhe, e uma strategy nova nasce no IR.
3. SourceMapGenerator e SourceMapComposer
O SourceMapGenerator escreve um mapa V3 padrão (Base64 VLQ) de cada módulo TypeScript que o eqc emite de volta às linhas do .cs de onde ele veio, nomeando cada arquivo a partir do qual o módulo foi escrito (uma classe que adota o padrão de uma interface leva esse padrão ao arquivo da interface), cada um dentro do projeto, e com o próprio C# dentro só quando o mapa é full. O bun então mapeia o JavaScript para esse TypeScript, e o SourceMapComposer compõe os dois, em C# e sem pacote npm nenhum, num ÚNICO mapa do JavaScript que o browser roda até o C#, mantendo o debugId do bun. Os dois mapas passam por um só escritor de mapa, e esse escritor, como todo arquivo JSON que o build escreve, por um só escritor de JSON cujo escape é o do System.Text.Json, então um arquivo C# com um caractere de controle continua tendo mapa. O que um mapa carrega (EQuanticSourceMaps: full em Debug, none no resto, external quando pedido) está em Depuração. 4. Símbolos primeiro, nomes só onde é honesto
O conversor pergunta ao modelo semântico antes de adivinhar. Um membro, um local, um parâmetro, um estático — cada um emite a partir do seu símbolo (this.name, Counter.name, um nome puro), e Console.WriteLine vira console.log porque o símbolo diz System.Console. Heurísticas por nome (um sublinhado inicial, uma propriedade capitalizada) só são legítimas onde o modelo não pode ser consultado — um trecho sem modelo, um nó reescrito por uma strategy. Com um modelo autoritativo, um nome na árvore que não vincula é erro de build (EQ2006), nunca uma tradução adivinhada.
5. O IR e os seus escritores
Desde 0.2.0-preview.36
CodeGen/Ir/ é uma árvore pequena com um escritor por nível — JsExpr → JsStatement → JsClassMember → JsClass → JsModule — e os escritores são donos de tudo o que uma strategy nunca deve escrever à mão:
•
Parênteses vêm de precedência e associatividade, nunca de um template. Antes disto, f ?? g && g saía literal: o C# não precisa de parênteses ali, o JavaScript recusa a mistura nua, e o bundle inteiro deixava de parsear.
•
Avaliação única: um JsTemplate diz o que computa ({0} === {0}.normalize()) e o escritor liga uma parte usada mais de uma vez exatamente uma vez — nome puro ou literal é inlined, leitura de membro não (um getter pode contar). Uma parte dentro de uma função que o template define também é ligada, nome puro incluído, porque essa função roda uma vez por elemento: só um literal, this e uma lambda escrita no lugar ficam dentro, e também uma parte que lê um nome que a função declara.
•
Layout: um statement por linha, blocos indentados (JsLayout.Pretty); Compact reproduz o antigo mundo de string byte a byte, e é assim que cada passo da migração é provado. Uma classe tem uma regra só de layout — linha em branco antes de um membro com corpo, campos contíguos — e um módulo são os seus imports como registos JsImport, uma linha em branco, o corpo.
•
O emitter (TypeScriptEmitter) decide o que um módulo contém e entrega nós ao builder; não monta texto.
Duas redes seguram tudo isso: os pins dos componentes (o módulo gerado de cada componente compartilhado, byte a byte — uma mudança de layout tem de ser só-whitespace contra eles) e a suíte de conformance, que executa cada forma traduzida dos dois lados e compara as respostas. Os gêmeos gerados em src/eQuantic.UI.Runtime/src/shared/components são fixados, tipados e testados — não lintados: código gerado responde ao seu escritor, não a um guia de estilo.
Hoje o compilador suporta uma ampla gama de construções do C#:
•
Expressões: aritméticas, lógicas, ternárias, interpolação de string, null-coalescing (??), acesso condicional (?., ?[]). Um argumento que aguarda atrás de ?. é aguardado onde está escrito, só quando o receptor não é nulo (Desde 0.2.0-preview.60), seja qual for o receptor: uma chamada, um elemento ou uma propriedade na frente de ?. é lida uma vez para uma variável do método onde está escrita (Desde 0.2.0-preview.61; até então um receptor assim falhava o build com EQ1004). Uma leitura null-conditional responde null onde o seu valor é usado, como em C#, nunca o undefined do JavaScript (Desde 0.2.0-preview.61). Um method group é o delegate que o C# cria: o seu receptor é lido uma vez, quando o delegate é criado, base.M chama o método da base sobre este objeto, e o grupo de um método de extensão é ligado na classe que o declara, para onde vai a sua chamada. Um grupo sobre uma extensão que nada no bundle declara, como list.Any, falha o build com EQ2004 (Desde 0.2.0-preview.61)
•
Controle de fluxo: if, switch, for, foreach, while, do-while, break, continue, throw
•
Padrões modernos: suporte completo a padrões recursivos, de propriedade, posicionais, relacionais e lógicos (C# 9.0 - 12.0)
•
Gestão de recursos: suporte a instruções using e declarações using var
•
Exceções: suporte completo a try-catch-finally e a instruções throw (Exception → Error)
•
Índices e intervalos: suporte ao operador de índice a partir do fim (array[^1] → array[array.length - 1])
•
Métodos de string: métodos de instância (Split, Replace, StartsWith, EndsWith, Contains, Substring, IndexOf, LastIndexOf, PadLeft, PadRight, Trim, TrimStart, TrimEnd, ToUpper, ToLower, ToUpperInvariant, ToLowerInvariant, Insert, Remove, ToCharArray) e métodos estáticos (IsNullOrEmpty, IsNullOrWhiteSpace, Join, Concat, Compare, Equals, Format). Um método com um StringComparison responde por ele, pelo runtime, e uma busca por uma comparação de cultura falha o build (EQ1004): o navegador não tem busca sensível à cultura (Desde 0.2.0-preview.60)
•
Métodos numéricos: Parse e TryParse de todo tipo numérico (int, uint, long, ulong, short, ushort, byte, sbyte, float, double, decimal), lidos pelo runtime com a gramática do .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
•
Interfaces de lista: um elemento lido ou escrito por IList<T>, IReadOnlyList<T> ou IList → $eq.collections.item(list, i) e $eq.collections.setItem(list, i, value), o seu Count → $eq.collections.count(list), e o Add e o Clear próprios de ICollection<T> → $eq.collections.add(collection, item) e $eq.collections.clear(collection). Cada um responde pela coleção que a interface guarda quando roda: um array pelo seu subscrito, o set, a lista ligada ou o dicionário do runtime pelos seus próprios membros, e um tipo seu pelo seu indexador, o seu Count, o seu Add e o seu Clear (Desde 0.2.0-preview.61)
•
Métodos estáticos de Array: suporte completo aos métodos estáticos de Array
•
Array.Sort(array) → array.sort() - ordena o array no lugar
•
Array.Sort(array, comparison) → array.sort(comparison) - ordena com comparador próprio
•
Array.Reverse(array) → array.reverse() - inverte o array no lugar
•
Array.Find(array, predicate) → array.find(predicate) - acha o primeiro elemento que casa
•
Array.FindIndex(array, predicate) → array.findIndex(predicate) - acha o índice do primeiro que casa
•
Array.FindAll(array, predicate) → array.filter(predicate) - acha todos os elementos que casam
•
Array.IndexOf(array, value) → array.indexOf(value) - acha o índice de um valor
•
Array.LastIndexOf(array, value) → array.lastIndexOf(value) - acha o último índice de um valor
•
Array.Exists(array, predicate) → array.some(predicate) - confere se algum elemento casa
•
Array.TrueForAll(array, predicate) → array.every(predicate) - confere se todos os elementos casam
•
Array.Clear(array) → array.splice(0) - limpa todos os elementos
•
Array.Resize(ref array, size) → array.length = size - redimensiona o array
•
Métodos de enum: um enum não tem objeto próprio no navegador, então os estáticos de Enum chamam as funções de enum do runtime, $eq.enums, com a forma do enum escrita na chamada: os nomes, os nomes em camelCase que o navegador guarda, os valores e se é um enum [Flags] (Desde 0.2.0-preview.60; nomeavam um objeto que nenhum módulo declara, e lançavam)
•
Enum.Parse<T>(texto) e Enum.Parse(typeof(T), texto) → $eq.enums.parse(texto, forma): um nome, nomes separados por vírgulas ou um número, a caixa mantida a menos que ignoreCase seja true, e um erro onde o .NET lança um
•
Enum.TryParse<T>(texto, out var result) → true com o valor, ou false com o padrão do enum em result, e null na sobrecarga que recebe um Type
•
Enum.GetName(value) → o nome do membro com esse valor, ou null onde nenhum o tem
•
Enum.GetNames<T>() e Enum.GetValues<T>() → os nomes e os valores, na ordem dos valores
•
Enum.IsDefined(typeof(T), value) → se algum membro tem esse valor, dado como o enum, um número, um nome declarado, ou um object que guarde qualquer um deles
•
value.ToString(), uma interpolação e uma concatenação escrevem o nome do membro, as flags ligadas de uma combinação (Read, Write), um valor que nenhum membro nomeia como os seus dígitos, e nada para um enum anulável nulo. ToString("D"), "X", "F" e "G", e o formato de uma interpolação, escrevem o que o .NET escreve, e um alinhamento preenche esse texto
•
Um cast de um object que guarda o enum mantém o valor ((Status)Enum.Parse(typeof(Status), texto)), um cast entre dois enums passa pelos valores, e um valor que nenhum membro nomeia fica guardado como o seu número
•
Métodos de Dictionary: todo Dictionary, IDictionary e IReadOnlyDictionary, e os ordenados, é uma classe do runtime em $eq.collections.*, consultada com has/get (Desde 0.2.0-preview.60)
•
new Dictionary<K, V>() → $eq.collections.dictionary(), ou $eq.collections.dictionary(null, true) quando o tipo da chave compara por valor (um record, uma struct, uma tupla, um decimal, uma data, uma classe que sobrescreve Equals); uma cópia a semeia com a sua fonte, e um inicializador com pares [key, value]
•
dict[key] → $eq.mapGet(dict, key) - lança para uma chave ausente como o .NET; dict[key] = value → $eq.mapSet(dict, key, value), que responde o valor escrito
•
ContainsKey(key) → dict.has(key) - nunca responde por um membro do protótipo
•
TryGetValue(key, out var value) → (dict.has(key) ? ((value = dict.get(key)), true) : ((value = 0), false)) - uma chave ausente escreve default(TValue) (0 aqui, para um int)
•
GetValueOrDefault(key[, defaultValue]) → (dict.has(key) ? dict.get(key) : 0) - um padrão explícito é avaliado uma vez, seja ou não necessário
•
Um receptor ou uma chave lidos por um membro ou por uma chamada são vinculados uma vez: this.cache.TryGetValue(Next(), out var v) → (($0, $1) => …)(this.cache, next())
•
Add(key, value) → dict.set(key, value) - NÃO lança para uma chave que já existe, onde o do .NET lança (#440) •
Remove(key) → dict.delete(key), e Remove(key, out var value) move o valor para o out
•
TryAdd(key, value) → dict.tryAdd(key, value), ContainsValue(value) → dict.containsValue(value)
•
Keys / Values (propriedades) → dict.keys() / dict.values(), arrays na ordem do dicionário; Count, Keys.Count e Values.Count → dict.size, e Keys.Contains(key) → dict.has(key)
•
LINQ: conversão direta dos métodos LINQ para os equivalentes em JS:
•
Projeção: Select → map, SelectMany → flatMap
•
Filtragem: Where → filter, Distinct → [...new Set()]
•
Ordenação: OrderBy/OrderByDescending → sort, Reverse → [...arr].reverse()
•
Particionamento: Skip → slice(n), Take → slice(0, n)
•
Elemento: First/FirstOrDefault → find/[0], Last/LastOrDefault → arr[arr.length-1], Single/SingleOrDefault → find/[0]; com um valor padrão, FirstOrDefault, LastOrDefault e SingleOrDefault → $eq.linq.firstOrDefault, $eq.linq.lastOrDefault e $eq.linq.singleOrDefault, que o respondem onde nada casa (Desde 0.2.0-preview.62)
•
Quantificadores: Any → some/length > 0, All → every, Contains → includes
•
Agregação: Count → length/filter().length, Sum → reduce(($a, $b) => $a + $b, 0), Average → $eq.linq.average, que calcula a média no tipo do elemento, um long, um float, um decimal ou um anulável (Desde 0.2.0-preview.62), Min → Math.min(...), Max → Math.max(...)
•
Concat(other) → [...source, ...other] - concatena duas sequências
•
Union(other) → [...new Set([...source, ...other])] - elementos únicos das duas sequências
•
Intersect(other) → [...new Set(source)].filter(($x) => other.includes($x)) - elementos em comum
•
Except(other) → [...new Set(source)].filter(($x) => !other.includes($x)) - elementos na origem mas não na outra
•
other, como todo argumento de uma chamada LINQ que não é uma lambda escrita no lugar, é avaliado uma vez, na ordem do C#, antes de o lowering rodar: rodava uma vez por elemento (Desde 0.2.0-preview.61)
•
Pares: Zip(second) e Zip(second, third) → $eq.linq.zipTuples, que responde os pares e as triplas que o .NET responde (Desde 0.2.0-preview.62)
•
Igualdade: SequenceEqual → $eq.linq.sequenceEqual, que compara pela igualdade padrão do tipo do elemento (Desde 0.2.0-preview.62)
•
Comparadores: um comparador passado a ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet ou SequenceEqual que pede o padrão (EqualityComparer<T>.Default, null, StringComparer.Ordinal) é descartado, já que o rebaixamento acha as chaves desse jeito, e qualquer outro faz a build falhar com EQ2007 (Desde 0.2.0-preview.61)
•
Cast<T>() → repasse (o JavaScript tem tipagem dinâmica)
•
OfType<T>() → filter(($x) => typeof $x === 'type') para primitivos, filter(($x) => $x instanceof Type) para objetos
•
Async/await: mapeamento de Task para Promise e suporte nativo a await.
•
Operadores modernos do C#: suporte aos operadores e palavras-chave modernos
•
Atribuição null-coalescing: x ??= value → x ?? (x = value) - atribui só se for nulo/indefinido
•
Operador nameof: nameof(variable) → 'variable' - pega o nome como string em tempo de compilação. É o nome, não a grafia: nameof(@class) → 'class' (Desde 0.2.0-preview.60)
•
Palavra-chave default: default(T) e o literal default são o default do tipo, e o tipo do literal é aquele para o qual o C# o converte: default(int) → 0, default(long) → 0n, default(int?) → null, int x = default → 0 (Desde 0.2.0-preview.58)
O que o C# te dá de graça, e o JavaScript não
Dois padrões são implícitos no C# e ausentes no JavaScript. Os dois foram emitidos como nada por um tempo, e os dois falham TARDE, não em tempo de build, e não onde está a causa.
Um tipo de valor não definido é ZERO
Desde 0.2.0-preview.22
Um campo de tipo de valor é zero tenha alguém escrito = 0 ou não. No cliente ele era undefined, e os dois não são o mesmo valor. As leituras sobrevivem por sorte enquanto forem TESTES (undefined > 0 é falso, que é o que 0 teria dito), e aí a primeira ARITMÉTICA o transforma em NaN. Um Math.max(width, undefined) chega à folha de estilo como width:NaNpx, uma regra que o parser de CSS descarta inteira: a classe é computada, hasheada, emitida, posta no elemento, e não faz nada.
Isso aparece só em páginas RENDERIZADAS no cliente, nunca no SSR nem numa carga direta, porque o servidor computa a mesma propriedade em C#, onde ela era 0 o tempo todo. Então os dois alvos discordam sobre um campo e a página que prova isso é a que ninguém recarrega.
Todo tipo de valor não anulável agora carrega o padrão dele (0, false, o membro zero de um enum). Os anuláveis não, porque ali null É a resposta do C#, e inventar um zero seria a mesma divergência apontando para o outro lado.
Um parâmetro de construtor primário é ESTADO de instância
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
O Roslyn modela a captura como um IParameterSymbol, então todo lugar que pergunta "isto é um parâmetro?" responde sim sobre algo que se comporta como campo. Emitido cru ele compila, a página renderiza, e o ReferenceError chega quando o callback finalmente dispara, aparecendo de dentro do reconciliador como um TypeError sobre outra coisa completamente diferente.
Modelos comuns também atravessam
Um componente não é o único C# de que uma página precisa. O modelo de documento por trás de um editor, uma pequena máquina de estados, um parser: nenhum deles é componente, e todos eles têm que rodar nos dois alvos. Eles transpilam do mesmo jeito, como módulos próprios, e as regras abaixo são o que torna a emissão CONFERIDA em vez de meramente presente.
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)
O último formato é o que o JavaScript não consegue dizer diretamente: ^0 significa o FIM, enquanto slice(0, -0) é slice(0, 0), que é vazio. Qualquer coisa que não seja um literal positivo depois de ^ resolve, portanto, contra o comprimento do jeito que o Index.GetOffset faz. Um Range guardado como VALOR é reportado (EQ2004): nada do outro lado recebe um, e indexar no ponto de uso é para o que ele serve.
Um intervalo sobre um tipo seu com um Length (ou um Count) e um Slice(int start, int length) não é a fatia de um array: o C# o rebaixa para esse Slice, que recebe um início e um COMPRIMENTO onde o slice do JavaScript recebe um fim, e o twin também (Desde 0.2.0-preview.61):
strip[1..3] → strip.slice(1, 3 - 1)
O receptor é lido uma vez, depois os extremos na ordem em que estão escritos, depois o Length ou o Count, lido só onde um extremo conta a partir do fim ou o fim fica aberto, como o .NET os lê. Um intervalo passado a um indexador que recebe o próprio Range lhe passaria um valor Range, e faz a build falhar (EQ2004) pelo mesmo motivo que um guardado faz.
O JavaScript não tem nenhum dos dois. Um método que os declara devolve um OBJETO (o valor dele sob $, cada out e ref sob o nome deles) e o corpo dele se move para dentro de um closure para que todo return ali continue significando o que significava. O ponto de chamada desembrulha com uma arrow, o que funciona em qualquer posição de expressão, incluindo dentro de um if:
var next = document.Replace(range, text, out var caret);
let next = ($o => (caret = $o.caret, $o.$))(document.replace(range, text));
O out sai da lista de parâmetros do JS (ele não é passado PARA DENTRO); o ref fica, porque é lido antes de ser escrito. out _ não atribui nada.
Variáveis de expressão são declaradas onde o C# as declara
A variável de um padrão (x is T t), um out var e o elemento de uma desconstrução ((var c, var d) = …) são atribuídos dentro da expressão que os declara, então a instrução que contém essa expressão os declara, com o escopo que o Roslyn dá a eles (Desde 0.2.0-preview.59):
•
uma instrução de expressão, um if, um return, um throw, um yield return, uma declaração, a expressão governante de um switch e um lock os declaram na frente de si mesmos, onde o código seguinte pode lê-los (if (!int.TryParse(s, out var n)) return; e depois n). Quando estão direto numa seção de um switch, deixam-nos para o switch, que os declara uma vez para o bloco inteiro, como o C# os escopa, e assim outra seção pode atribuí-los;
•
um while, um do e um for os declaram no cabeçalho do próprio laço, for (let n; …), que o JavaScript copia a cada iteração, como o .NET dá à condição de um laço uma variável nova: um closure criado numa iteração guarda o valor daquela iteração;
•
um foreach e um using os declaram num bloco em volta da instrução, que é o escopo deles no C#;
•
uma lambda, uma função local, a cláusula de uma consulta e o braço de uma expressão switch declaram os seus, uma vez a cada chamada;
•
o inicializador de um campo ou de uma propriedade os declara numa arrow só dele.
Um nome que o JavaScript recusa e que o escape verbatim do C# permite (@class, @new, @default) vira um único nome legal na declaração e em toda referência, qualquer que seja a forma como foi ligado: class$, com um $ que nenhum nome C# tem. Um label e o parâmetro de tipo de um método escritos assim recebem o mesmo nome, e um membro ou uma chave de with é o próprio nome: r.class, { class: 5 } (Desde 0.2.0-preview.61).
Um local com o nome de um global que o código emitido lê (crypto, undefined, Math, Number, console) recebe o mesmo $, então o global continua visível para o código ao lado, e todo nome que o código emitido declara para si começa com $, então nunca encontra um local que o código em volta lê (Desde 0.2.0-preview.61).
Coleções: capacidade não é conteúdo
new List<T>(x) significa duas coisas opostas dependendo do que x é, e só o construtor resolvido consegue dizer qual: new List<string>(other.Count) é uma lista vazia dimensionada de antemão, new List<string>(other) é uma cópia. O primeiro emite um array vazio, o segundo [...other]. Com um inicializador de coleção, a lista é o que o construtor pôs nela, e depois cada elemento em ordem: new List<string>(other) { "x" } é [...other, "x"] e new List<string>(8) { "x" } é ["x"], escrita por extenso ou com o tipo do alvo. Uma capacidade que não é uma constante que a lista aceita ainda é avaliada, antes dos elementos, e uma negativa é recusada como o construtor de List<T> a recusa: new List<string>(Size()) { "x" } é ($eq.collections.listCapacity(this.size()), ["x"]) (Desde 0.2.0-preview.61).
Aritmética de char computa em unidades de código
Um char do C# em + - * / % promove a int e computa na unidade de código, enquanto um char transpilado é uma string de comprimento 1. Quando o tipo do RESULTADO é numérico, os operandos char rebaixam para unidades de código (literais constantes dobram para o número; expressões leem charCodeAt(0)), então text[i] - '0' é o dígito e 'A' + col é um número, exatamente como no .NET. O (char)numeric rebaixa para String.fromCharCode. char + string continua sendo concatenação: o tipo do resultado é string, então o ramo numérico nunca o vê.
Um valor do servidor atravessa uma fronteira TIPADA
O JavaScript não tem decimal nem inteiro de 64 bits, portanto o fio envia-os como strings — um long como "9007199254740993", um decimal como "0.1", a família de datas em texto ISO. O compilador sabe o tipo C# de cada campo de estado e de cada retorno de Server Action, e escreve esse conhecimento no gémeo como um mapa static $hydration; o runtime coage UMA vez, onde o valor chega, em vez de cada sítio de uso coagir defensivamente. Um record nomeia o seu próprio mapa, uma lista é [spec], os valores de um dicionário são { dict: spec }, e um tuplo é posicional ({ tuple: [...] }) porque atravessa como array. Nada é emitido onde hidratar seria a identidade, que é a maioria dos campos.
Um default é decidido pelo TIPO, e nunca é null para tipo de valor
new int[0].SingleOrDefault() é 0 no .NET, não null, e o mesmo vale para FirstOrDefault, LastOrDefault, ElementAtOrDefault e DefaultIfEmpty. Um campo declarado sem inicializador toma o mesmo default: um int é 0, um bool false, um long 0n, e um ENUM o seu membro de valor zero — que deste lado é uma string com o NOME do membro, portanto um campo por definir renderizaria nada. Uma tabela só responde pelos dois casos, por símbolo, para que um tipo alcançado por alias (using Amount = decimal;) não tenha resposta diferente do próprio tipo.
A mesma tabela preenche um array dimensionado, new T[n], e responde default(T) e o literal default, cujo tipo é aquele para o qual o C# o converte: o de uma variável local, o de um parâmetro, o do outro braço de uma condicional. Um elemento struct é uma instância zero própria, porque fill compartilharia um objeto por todos os slots, e os twins de tempo e de identidade (DateTime, TimeSpan, DateOnly, TimeOnly, DateTimeOffset, Guid) começam no seu MinValue, Zero ou Empty. Um struct cujo twin escrito à mão não sabe construir o seu zero mantém o default do próprio twin num literal default: undefined, que o construtor do twin transforma no seu default, onde null o contornaria. Desde 0.2.0-preview.58.
Um segundo OrderBy reinicia a ordenação
xs.OrderBy(a).OrderBy(b) ordena por b. A ordenação anterior não permanece como chave primária — sobrevive apenas como o desempate que uma ordenação estável lhe dá — portanto é um resultado diferente de xs.OrderBy(a).ThenBy(b) sempre que b tem empates que a desfaria. Os dois atravessam fielmente: um OrderBy encadeado ordena primeiro a sua fonte e ordena isso, e o ThenBy compõe a sua chave na mesma comparação.
Uma metade arredonda para o vizinho PAR, como no .NET
Math.round(32.5) é 33 no JavaScript e 32 no .NET, que arredonda a metade para o par. Em todo o sítio onde o runtime espelha um MathF.Round usa a regra do .NET, para que um valor calculado no browser seja o valor que o servidor calculou — é isso que mantém a altura de linha de uma escala tipográfica, e qualquer layout derivado dela, idêntica na hidratação em vez de meio pixel ao lado.
É a aritmética do .NET, não uma aproximação dela. O meio é EXATO: Math.Round(0.5015, 3) dá 0.501, porque 0.5015 * 1000 é 501.49999999999994 — um valor perto da metade não é a metade. Todo modo de MidpointRounding é respeitado, uma contagem de dígitos acima de 15 (6 para um float) lança como no .NET, e o MathF.Round(x, digits) escala e divide em precisão simples.
A superfície do LINQ é uma tabela, e a numérica da BCL é outra
A maioria dos operadores LINQ é uma forma só — Where é filter, Aggregate é reduce com os argumentos trocados — portanto vivem como entradas numa tabela indexada por operador e número de argumentos, com o gate escrito uma vez e o escritor do IR a pontuar e a ligar qualquer recetor usado duas vezes. Um operador que tem de RACIOCINAR guarda estratégia própria: a família OrDefault lê o tipo do elemento, o Sum escolhe semente por ele, Cast e OfType testam tipos, Contains escolhe entre igualdade de identidade e estrutural. A mesma forma vale para a BCL numérica, onde a superfície moderna (Double.AcosPi e família) é uma tabela de templates em vez de um método cada.
Onde essas tabelas não têm entrada, a chamada é ERRO de build a nomear o membro, nunca um palpite: um nome emitido sem nada por trás é um ReferenceError no carregamento, e uma página que nunca renderiza.
Inteiros de largura fixa assentam pelo tipo
Um byte do C# passando de 255 envolve; um número do JavaScript continua contando. O compilador lê o tipo do RESULTADO de cada + - * << (e de ++, -- e das formas compostas) e assenta o valor onde o C# assentaria: byte, sbyte, short, ushort e uint SEMPRE envolvem (& 0xFF, << 16 >> 16, >>> 0 — valores empacotados e hashes dependem disso, então h *= 16777619 num uint é o passo FNV que é no .NET, via Math.imul). int e long só envolvem onde você escreveu unchecked (unchecked(a * a) é 0 para 65536; um long envolve via BigInt.asIntN), porque senão todo i + 1 comum numa UI carregaria um | 0 por um overflow que é bug em qualquer outro lugar — então um int.MaxValue + 1 sem mais continua a contagem do double, um limite documentado. Um contexto checked — o bloco, a expressão ou a configuração do projeto inteiro, lida do IsChecked da árvore vinculada e não da sintaxe — lança overflow exatamente onde o C# lança. Um float é um single em todo lugar onde é PRODUZIDO — cada + - * /, cada incremento e atribuição composta num alvo float (num float? ainda não arredonda), um int acima de 2^24 entrando, uma constante float, um valor hidratado do servidor —, porque o RyuJIT arredonda cada operação de float, e assim a*x - b*x dá no browser o mesmo último bit do servidor. Ele imprime como o decimal mais curto que lê de volta o mesmo single, então 0.1f + 0.2f é "0.3" dos dois lados. char++ avança o caractere; um enum em aritmética (day + 1, a.CompareTo(b)) computa no valor por trás do nome do membro.
Conversões implícitas assentam pela árvore vinculada
A sintaxe nunca mostra uma conversão implícita — int i = c com um char, Twice(c), a[c], long l = n — e uma regra escrita contra a sintaxe só cobre as formas de que o autor se lembrou. O compilador lê-as da árvore vinculada do Roslyn: depois de cada expressão traduzida, a conversão em que a árvore vinculada a embrulha é aplicada (ValueFlow), em todo sítio onde o C# a aplica — inicializadores, argumentos, índices, retornos, comparações. Um char promovido a número vira a sua unidade de código (uma constante dobra: 'A' + col é 65 + col); um int a fluir para um long vira BigInt (1 → 1n, e é por isso que TimeSpan.FromSeconds(90) emite 90n: a sobrecarga do .NET 9 recebe um long). Dois chars comparados continuam caracteres — o JavaScript ordena strings de 1 caractere pelas mesmas unidades de código. Uma conversão definida pelo usuário atravessa como uma chamada ao estático que o gêmeo carrega — um tipo do próprio app como Money.fromInt(5), um tipo do vocabulário como IconGlyph.fromIcons('search') para Icon(Icons.Search) —, a não ser que o gêmeo receba o operando como ele é: o SizeValue a partir de um número declara exatamente isso, então Width = 120 continua um simples 120, e um tipo de fora do SDK (Index) passa a sua primitiva adiante. Um valor a caminho de TEXTO assenta da mesma forma: embalado numa concatenação, operando string de uma, ou buraco simples de interpolação, imprime como o C# imprime — null como nada, um bool como "True", um enum pelo nome do membro — s += flag incluído, que nenhuma regra sintática vira.
Os operadores próprios de um tipo atravessam
O JavaScript não sobrecarrega +, então um record ou struct que você declara com operadores leva cada um no gémeo como método estático, e todo sítio em que a árvore vinculada mostra um operador o chama: a + b é Money.opAdd(a, b), -m é Money.opNegate(m) (o - unário e o binário têm nome por aridade, então nunca colidem), m += other é m = Money.opAdd(m, other). Conversões também: implicit operator Money(int v) vira Money.fromInt, chamado onde o C# converte — uma declaração, um argumento, o operando de um compound — e explicit operator int(Money m) vira Money.toInt, chamado pelo cast. Só para tipos no seu código: um wrapper do framework como SizeValue ou Index é a sua primitiva deste lado, e os operadores dele passam o valor adiante.
O ELEMENTO de um foreach converte da mesma forma, um item de cada vez: foreach (long l in ints) faz de cada int um BigInt, foreach (int code in chars) de cada char a sua unidade de código, foreach (Money m in ints) chama a conversão do tipo — o laço liga um $l cru e declara l a partir dele. Uma DECLARAÇÃO using (using var r = …;) é dona do resto do bloco: o que se segue corre dentro de um try cujo finally descarta r — depois de o valor de retorno ser tomado, quando o corpo lança, em ordem inversa quando vários partilham um bloco — e await using aguarda o disposeAsync.
Dicionários enumeram como os do .NET
Um dicionário é a classe de dicionário do runtime, que guarda as entradas por posição como o Dictionary do .NET: enumera na ordem de inserção enquanto nada é removido, e uma entrada adicionada depois de uma remoção ocupa a posição da removida, a última liberada primeiro. Um foreach sobre ele (e o new List<KeyValuePair<,>>(dict), que o espalha) produz pares que desconstroem como [key, value] E respondem .key/.value (as duas formas de consumo do C#), cada chave no seu tipo, então o próximo key + 1 soma. Um dicionário ordenado produz os mesmos pares, na ordem das chaves. Desde 0.2.0-preview.60.
Anotações de item genérico deixam para a inferência
new List<KeyValuePair<int,float>>(…) não consegue anotar o let dele com o nome C# cru (KeyValuePair[] não nomeia nada em TS); itens genéricos deixam a anotação para a inferência.
Statics inicializam como um TIPO, no primeiro uso
O C# inicializa os statics de um tipo juntos: cada um começa no seu zero, depois os inicializadores rodam na ordem da declaração, depois o construtor estático, uma vez, no primeiro uso. Um tipo cujos statics podem observar uns aos outros (um inicializador que não é constante, ou um construtor estático) guarda todos num objeto $slots que o $init() monta na primeira vez que um deles é lido ou escrito, e cada static é um par de acessores sobre o seu slot, então static int A = B + 1; acima de static int B = 2; lê o zero de B e responde 1, como o .NET, e dois tipos cujos statics leem um ao outro veem o zero que o .NET vê. O mesmo mecanismo serve a um record, a uma struct, a uma classe, a uma classe estática e a um componente. Por ser preguiçoso, ele também sobrevive aos ciclos de import da biblioteca compartilhada (os seus módulos se importam por um barril só, e o que carrega primeiro vê a classe do outro como undefined), que era para o que servia o getter preguiçoso por static que ele substitui; esse getter inicializava cada static na sua própria primeira leitura, sem ordem nenhuma. Um tipo cujos inicializadores são constantes continua com campos simples, cada um escrito como o VALOR a que o C# o reduz (static readonly int Max = Default * 2; acima de const int Default = 50; é 100, onde a expressão lia Default antes de a sua declaração o definir), e um static cujo zero monta um twin também é montado no primeiro uso, nunca enquanto o seu módulo é avaliado. Um tipo que declara um construtor estático também inicia a inicialização primeiro no seu construtor de instância e em cada método, acessor e operador estático, já que o C# roda esse construtor antes da primeira instância e do primeiro uso de qualquer membro estático. Os statics inicializam na ordem da declaração entre campos, propriedades e eventos de campo; o $slots existe antes de qualquer coisa rodar, então uma leitura reentrante vê o zero que o C# vê; o corpo do construtor estático roda numa função própria depois dos inicializadores, então um return nele termina só ele; e uma falha fica guardada, e todo uso do tipo passa a lançar uma TypeInitializationException cuja exceção interna é a original, como o .NET faz. Desde 0.2.0-preview.61.
O que uma ANOTAÇÃO de tipo pode nomear
O .ts emitido é checado por tipos (essa é a segunda das duas camadas), então uma assinatura nunca pode introduzir um nome que o módulo não consiga resolver:
um enum
string, já que a representação em execução é o nome do membro
uma interface sem gêmeo emitido
any
IReadOnlyList<(char, char)>
[string, string][]
Action<T>?
((t: T) => void) \| null, entre parênteses, ou a união liga ao retorno
um nome que nada consegue verificar
any, porque um tipo errado é pior que um aberto
Os records carregam as mesmas regras, mais os campos estáticos deles e as propriedades computadas (um record é um valor com COMPORTAMENTO, não só os membros posicionais dele).
A biblioteca É o diretório dela
Tanto o conjunto transpilado quanto o barril de exports do runtime são gerados a partir do diretório de código, nunca de uma lista mantida à mão, então a biblioteca embarcada nunca consegue se afastar do código a partir do qual ela é construída.
Código C#:
private void Increment() {
if (Count > 10) Console.WriteLine("Max reached");
Saída TypeScript:
if (this.count > 10) console.log("Max reached");
🎯 Exemplos de recursos avançados
Código C#:
public enum OrderStatus { Pending, Processing, Shipped, Delivered }
private void HandleStatusChange(string input)
// Faz o parse do enum a partir de uma string (sem diferenciar maiúsculas)
if (Enum.TryParse<OrderStatus>(input, ignoreCase: true, out var status))
Console.WriteLine($"Status changed to: {status}");
// Pega todos os valores do enum para um dropdown
var allStatuses = Enum.GetValues<OrderStatus>();
foreach (var s in allStatuses)
Console.WriteLine($"Available status: {s}");
// Valida um valor de enum
if (Enum.IsDefined(typeof(OrderStatus), "Shipped"))
Console.WriteLine("Valid status");
Saída TypeScript (shape está no lugar da forma do enum, que o eqc escreve em cada chamada: { names: ['Pending', 'Processing', 'Shipped', 'Delivered'], keys: ['pending', 'processing', 'shipped', 'delivered'], values: [0, 1, 2, 3], flags: false, digits: 8 }):
handleStatusChange(input: string) {
// Parse com TryParse: o padrão do enum quando falha, como o .NET deixa
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)}`);
// Pega todos os valores, na ordem dos valores
let allStatuses = $eq.enums.values(shape);
for (const s of allStatuses) {
console.log(`Available status: ${$eq.enums.text(s, shape)}`);
// Valida um nome declarado
if ($eq.enums.isDefined('Shipped', shape, 'name')) {
console.log('Valid status');
Código C#:
private Dictionary<string, int> _settings = new();
private void ManageSettings()
_settings.Add("timeout", 5000);
_settings.Add("retries", 3);
if (_settings.ContainsKey("timeout"))
var timeout = _settings["timeout"];
Console.WriteLine($"Timeout: {timeout}");
if (_settings.TryGetValue("maxItems", out var max))
Console.WriteLine($"Max: {max}");
foreach (var key in _settings.Keys)
Console.WriteLine($"{key} = {_settings[key]}");
Saída TypeScript:
this._settings = $eq.collections.dictionary();
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)}`);
Operações de conjunto do LINQ
Código C#:
private void ProcessCollections()
var list1 = new[] { 1, 2, 3, 4 };
var list2 = new[] { 3, 4, 5, 6 };
// Concatena as duas listas
var combined = list1.Concat(list2);
// Resultado: [1, 2, 3, 4, 3, 4, 5, 6]
// Union - elementos únicos das duas
var union = list1.Union(list2);
// Resultado: [1, 2, 3, 4, 5, 6]
// Intersect - elementos em comum
var common = list1.Intersect(list2);
// Except - elementos na list1 mas não na list2
var difference = list1.Except(list2);
// Filtragem complexa com operações de conjunto
var activeUsers = GetActiveUsers();
var premiumUsers = GetPremiumUsers();
// Usuários que são ativos E premium
var activePremium = activeUsers.Intersect(premiumUsers);
// Usuários que são ativos mas NÃO premium
var activeFree = activeUsers.Except(premiumUsers);
Saída TypeScript:
const list1 = [1, 2, 3, 4];
const list2 = [3, 4, 5, 6];
const combined = [...list1, ...list2];
// Union (com Set para remover duplicatas)
const union = [...new Set([...list1, ...list2])];
// Intersect (elementos em comum)
const common = [...new Set(list1)].filter(x => list2.includes(x));
const difference = [...new Set(list1)].filter(x => !list2.includes(x));
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
Código C#:
private void ProcessArrayOperations()
var numbers = new[] { 5, 2, 8, 1, 9 };
var items = new[] { "apple", "banana", "cherry" };
// Ordena o array no lugar
// Resultado: [1, 2, 5, 8, 9]
// Ordena com comparação própria
Array.Sort(items, (a, b) => b.Length - a.Length);
// Resultado: ["banana", "cherry", "apple"]
// Resultado: [9, 8, 5, 2, 1]
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);
var index = Array.IndexOf(numbers, 5);
var lastIndex = Array.LastIndexOf(numbers, 5);
// Operações de verificação
var hasEven = Array.Exists(numbers, n => n % 2 == 0);
var allPositive = Array.TrueForAll(numbers, n => n > 0);
Array.Resize(ref items, 5); // Expande para 5 elementos
Saída TypeScript:
processArrayOperations() {
const numbers = [5, 2, 8, 1, 9];
const items = ["apple", "banana", "cherry"];
items.sort((a, b) => b.length - a.length);
const firstEven = numbers.find(n => n % 2 == 0);
const firstEvenIndex = numbers.findIndex(n => n % 2 == 0);
const allEvens = numbers.filter(n => n % 2 == 0);
const index = numbers.indexOf(5);
const lastIndex = numbers.lastIndexOf(5);
// Operações de verificação
const hasEven = numbers.some(n => n % 2 == 0);
const allPositive = numbers.every(n => n > 0);
Filtragem por tipo no LINQ (Cast e OfType)
Código C#:
private void FilterByType()
// Coleção de tipos mistos
object[] mixed = new object[] { 1, "hello", 2, "world", 3.14, true };
// Cast<T>() - assume que todos os elementos são do tipo T (repasse em JS)
var assumedStrings = mixed.Cast<string>();
// OfType<T>() - filtra só os elementos do tipo T
var onlyStrings = mixed.OfType<string>();
// Resultado: ["hello", "world"]
var onlyNumbers = mixed.OfType<int>();
// Funciona com classes próprias também
var shapes = new object[] { new Circle(), new Square(), new Circle() };
var circles = shapes.OfType<Circle>();
// Resultado: [Circle, Circle]
// Filtragem de tipos primitivos
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]
Saída TypeScript:
// Coleção de tipos mistos
const mixed = [1, "hello", 2, "world", 3.14, true];
// Cast - repasse (o JS tem tipagem dinâmica)
const assumedStrings = mixed;
// OfType - filtra por typeof para primitivos
const onlyStrings = mixed.filter(x => typeof x === 'string');
// Resultado: ["hello", "world"]
const onlyNumbers = mixed.filter(x => typeof x === 'number');
// Resultado: [1, 2, 3.14]
// OfType - filtra por instanceof para objetos
const shapes = [new Circle(), new Square(), new Circle()];
const circles = shapes.filter(x => x instanceof Circle);
// Resultado: [Circle, Circle]
// Filtragem de primitivos
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 do C#
Código C#:
private void DemonstrateModernOperators()
// Atribuição null-coalescing (??=)
string? cachedData = null;
cachedData ??= LoadDataFromDatabase(); // Só carrega se for nulo
cachedData ??= "Default"; // Não executa, já foi atribuído
// Atribuição null-coalescing em propriedade
if (user.Settings ??= new Settings())
Console.WriteLine("Created new settings");
// Operador nameof (útil para binding de propriedade, validação)
var propertyName = nameof(user.Email);
Console.WriteLine($"Validating {propertyName}"); // "Validating Email"
var methodName = nameof(ProcessOrder);
LogAction(methodName); // "ProcessOrder"
// Palavra-chave default - valores padrão com tipo seguro
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
// Literal default (contextual)
int number = default; // 0 (inferido do tipo)
ProcessData(default); // passa o valor padrão do tipo do parâmetro
private void ProcessData(int value = default)
// value cai para 0 se não for fornecido
Saída TypeScript:
demonstrateModernOperators() {
// Atribuição null-coalescing
cachedData ?? (cachedData = this.loadDataFromDatabase());
cachedData ?? (cachedData = 'Default');
// Atribuição em propriedade
if (this.user.settings ?? (this.user.settings = new Settings())) {
console.log('Created new settings');
const propertyName = 'Email';
console.log(`Validating ${propertyName}`);
const methodName = 'ProcessOrder';
this.logAction(methodName);
let date = $eq.time.dateTime.minValue();
Métodos de string - exemplos adicionais
Código C#:
private void StringManipulation()
var text = " Hello World ";
var trimmed = text.Trim(); // "Hello World"
var leftTrim = text.TrimStart(); // "Hello World "
var rightTrim = text.TrimEnd(); // " Hello World"
var upper = text.ToUpper(); // " HELLO WORLD "
var lower = text.ToLower(); // " hello world "
var upperInv = text.ToUpperInvariant(); // " HELLO WORLD "
var lowerInv = text.ToLowerInvariant(); // " hello world "
var clean = text.Trim().ToLower().Replace("world", "everyone");
// Resultado: "hello everyone"
Saída TypeScript:
const text = " Hello World ";
const trimmed = $eq.text.trim(text);
const leftTrim = $eq.text.trimStart(text);
const rightTrim = $eq.text.trimEnd(text);
const upper = text.toUpperCase();
const lower = text.toLowerCase();
const upperInv = text.toUpperCase();
const lowerInv = text.toLowerCase();
const clean = $eq.text.trim(text).toLowerCase().replaceAll("world", "everyone");
Aparar passa pelo runtime porque o trim do JavaScript é um conjunto diferente do do .NET: deixa o U+0085 NEXT LINE, que o .NET apara, e leva o U+FEFF, que o .NET mantém. char.IsWhiteSpace, string.IsNullOrWhiteSpace e um Split() sem separador leem a mesma lista, por isso um texto se apara, se parte e se tokeniza igual dos dois lados (Desde 0.2.0-preview.58).