eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsCompilação
O compilador (CSharpToJs)
Editar esta página
34 min de leitura
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.
•
A ponte: a busca de dados passa por métodos anotados com [ServerAction] (estilo RPC). Veja Segurança e Server Actions.
🛠️ Componentes do compilador
1. TypeScriptEmitter
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.
2. CSharpToJsConverter
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.
🔄 Estratégias suportadas
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)
•
Clear() → dict.clear()
•
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(...)
•
Operações de conjunto:
•
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)
•
Filtragem por tipo:
•
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
1
2
3
4
5
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.
Intervalos são fatias
1
2
3
4
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):
1
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.
out e ref
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:
1
var next = document.Replace(range, text, out var caret);
1
2
let caret: any;
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:
C#
TypeScript
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
char
string
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.
📝 Exemplo de conversão
Código C#:
1
2
3
4
private void Increment() {
Count++;
if (Count > 10) Console.WriteLine("Max reached");
}
Saída TypeScript:
1
2
3
4
increment() {
this.count++;
if (this.count > 10) console.log("Max reached");
}
🎯 Exemplos de recursos avançados
Operações com enum
Código C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 }):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
handleStatusChange(input: string) {
let status: any;
// 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');
}
}
Operações com Dictionary
Código C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private Dictionary<string, int> _settings = new();
private void ManageSettings()
{
// Acrescenta entradas
_settings.Add("timeout", 5000);
_settings.Add("retries", 3);
// Confere a existência
if (_settings.ContainsKey("timeout"))
{
var timeout = _settings["timeout"];
Console.WriteLine($"Timeout: {timeout}");
}
// Obtenção segura
if (_settings.TryGetValue("maxItems", out var max))
{
Console.WriteLine($"Max: {max}");
}
// Itera as chaves
foreach (var key in _settings.Keys)
{
Console.WriteLine($"{key} = {_settings[key]}");
}
// Limpa tudo
_settings.Clear();
}
Saída TypeScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}
Operações de conjunto do LINQ
Código C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
// Resultado: [3, 4]
// Except - elementos na list1 mas não na list2
var difference = list1.Except(list2);
// Resultado: [1, 2]
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
processCollections() {
const list1 = [1, 2, 3, 4];
const list2 = [3, 4, 5, 6];
// Concatena
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));
// Except (diferença)
const difference = [...new Set(list1)].filter(x => !list2.includes(x));
// Filtragem complexa
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#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void ProcessArrayOperations()
{
var numbers = new[] { 5, 2, 8, 1, 9 };
var items = new[] { "apple", "banana", "cherry" };
// Ordena o array no lugar
Array.Sort(numbers);
// 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"]
// Inverte o array
Array.Reverse(numbers);
// Resultado: [9, 8, 5, 2, 1]
// Operações de busca
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);
// Operações de procura
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);
// Limpa e redimensiona
Array.Clear(numbers);
Array.Resize(ref items, 5); // Expande para 5 elementos
}
Saída TypeScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
processArrayOperations() {
const numbers = [5, 2, 8, 1, 9];
const items = ["apple", "banana", "cherry"];
// Ordena
numbers.sort();
// Ordena com comparação
items.sort((a, b) => b.length - a.length);
// Inverte
numbers.reverse();
// Operações de busca
const firstEven = numbers.find(n => n % 2 == 0);
const firstEvenIndex = numbers.findIndex(n => n % 2 == 0);
const allEvens = numbers.filter(n => n % 2 == 0);
// Operações de procura
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);
// Limpa e redimensiona
numbers.splice(0);
items.length = 5;
}
Filtragem por tipo no LINQ (Cast e OfType)
Código C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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>();
// Resultado: [1, 2]
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
filterByType() {
// 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#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
demonstrateModernOperators() {
// Atribuição null-coalescing
let cachedData = null;
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');
}
// Operador nameof
const propertyName = 'Email';
console.log(`Validating ${propertyName}`);
const methodName = 'ProcessOrder';
this.logAction(methodName);
// Palavra-chave default
let count = 0;
let text = null;
let flag = false;
let date = $eq.time.dateTime.minValue();
// Literal default
let number = 0;
this.processData(0);
}
processData(value = 0) {
// value cai para 0
}
Métodos de string - exemplos adicionais
Código C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void StringManipulation()
{
var text = " Hello World ";
// Aparando
var trimmed = text.Trim(); // "Hello World"
var leftTrim = text.TrimStart(); // "Hello World "
var rightTrim = text.TrimEnd(); // " Hello World"
// Conversão de caixa
var upper = text.ToUpper(); // " HELLO WORLD "
var lower = text.ToLower(); // " hello world "
var upperInv = text.ToUpperInvariant(); // " HELLO WORLD "
var lowerInv = text.ToLowerInvariant(); // " hello world "
// Encadeando métodos
var clean = text.Trim().ToLower().Replace("world", "everyone");
// Resultado: "hello everyone"
}
Saída TypeScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
stringManipulation() {
const text = " Hello World ";
// Aparando
const trimmed = $eq.text.trim(text);
const leftTrim = $eq.text.trimStart(text);
const rightTrim = $eq.text.trimEnd(text);
// Conversão de caixa
const upper = text.toUpperCase();
const lower = text.toLowerCase();
const upperInv = text.toUpperCase();
const lowerInv = text.toLowerCase();
// Encadeando
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).
NESTA PÁGINA
🛡️ Fronteiras (servidor vs cliente)
🛠️ Componentes do compilador
1. TypeScriptEmitter
2. CSharpToJsConverter
3. SourceMapGenerator e SourceMapComposer
4. Símbolos primeiro, nomes só onde é honesto
5. O IR e os seus escritores
🔄 Estratégias suportadas
O que o C# te dá de graça, e o JavaScript não
Um tipo de valor não definido é ZERO
Um parâmetro de construtor primário é ESTADO de instância
Modelos comuns também atravessam
Intervalos são fatias
out e ref
Variáveis de expressão são declaradas onde o C# as declara
Coleções: capacidade não é conteúdo
Aritmética de char computa em unidades de código
Um valor do servidor atravessa uma fronteira TIPADA
Um default é decidido pelo TIPO, e nunca é null para tipo de valor
Um segundo OrderBy reinicia a ordenação
Uma metade arredonda para o vizinho PAR, como no .NET
A superfície do LINQ é uma tabela, e a numérica da BCL é outra
Inteiros de largura fixa assentam pelo tipo
Conversões implícitas assentam pela árvore vinculada
Os operadores próprios de um tipo atravessam
Dicionários enumeram como os do .NET
Anotações de item genérico deixam para a inferência
Statics inicializam como um TIPO, no primeiro uso
O que uma ANOTAÇÃO de tipo pode nomear
A biblioteca É o diretório dela
📝 Exemplo de conversão
🎯 Exemplos de recursos avançados
Operações com enum
Operações com Dictionary
Operações de conjunto do LINQ
Métodos estáticos de Array
Filtragem por tipo no LINQ (Cast e OfType)
Operadores modernos do C#
Métodos de string - exemplos adicionais