Skip to content

Compiler pt BR

Edgar Mesquita edited this page Aug 22, 2026 · 11 revisions

O compilador (CSharpToJs)

🌐 Esta página em: English · Português

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

Gera Source Maps V3 padrão com codificação Base64 VLQ, mapeando o JavaScript/TypeScript gerado de volta às linhas originais do .cs ou .eqx.

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, Widget.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 — JsExprJsStatementJsClassMemberJsClassJsModule — 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).
  • 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 (?., ?[])
  • 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)
  • Métodos numéricos: int.Parse, double.Parse, float.Parse, decimal.Parse, long.Parse, int.TryParse, double.TryParse
  • 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
  • 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: suporte completo às operações de enum
    • Enum.Parse<T>(string)parseEnum(value, EnumType) (sem diferenciar maiúsculas)
    • Enum.TryParse<T>(string, out var result)(result = parseEnum(value, EnumType), result !== undefined)
    • Enum.GetValues<T>()Object.values(EnumType) - pega todos os valores do enum
    • Enum.GetNames<T>()Object.keys(EnumType) - pega todos os nomes dos membros
    • Enum.IsDefined(typeof(T), value)(EnumType[value] !== undefined) - valida um valor de enum
  • Métodos de Dictionary: suporte completo a Dictionary/IDictionary
    • ContainsKey(key)(key in dict) - confere se a chave existe
    • TryGetValue(key, out var value)(value = dict[key]) !== undefined - obtenção segura do valor
    • Add(key, value)dict[key] = value - acrescenta ou atualiza a entrada
    • Remove(key)delete dict[key] - remove a entrada
    • Clear()Object.keys(dict).forEach(k => delete dict[k]) - remove todas as entradas
    • Keys (propriedade) → Object.keys(dict) - pega todas as chaves como array
    • Values (propriedade) → Object.values(dict) - pega todos os valores como array
  • LINQ: conversão direta dos métodos LINQ para os equivalentes em JS:
    • Projeção: Selectmap, SelectManyflatMap
    • Filtragem: Wherefilter, Distinct[...new Set()]
    • Ordenação: OrderBy/OrderByDescendingsort, Reverse[...arr].reverse()
    • Particionamento: Skipslice(n), Takeslice(0, n)
    • Elemento: First/FirstOrDefaultfind/[0], Last/LastOrDefaultarr[arr.length-1], Single/SingleOrDefaultfind/[0]
    • Quantificadores: Anysome/length > 0, Allevery, Containsincludes
    • Agregação: Countlength/filter().length, Sumreduce((a,b) => a+b, 0), Averagereduce()/length, MinMath.min(...), MaxMath.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
    • 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 ??= valuex ?? (x = value) - atribui só se for nulo/indefinido
    • Operador nameof: nameof(variable)'variable' - pega o nome como string em tempo de compilação
    • Palavra-chave default: default(int)0, default(string)null, defaultundefined - pega o valor padrão do tipo

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.

Intervalos são fatias

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.

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:

var next = document.Replace(range, text, out var caret);
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.

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 [], o segundo [...other].

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.

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 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 resultado float é arredondado para precisão simples (Math.fround) e 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 (11n, 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 — e um operador implícito definido pelo utilizador passa o valor adiante, porque os wrappers do framework (SizeValue, Index, ColorToken) são a sua primitiva deste lado. 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 pares

Um Dictionary transpilado com chave primitiva é um objeto simples, e não iterável, então um foreach sobre ele (e o new List<KeyValuePair<,>>(dict)) rebaixa por $eq.entries(obj, numericKeys): pares que desconstroem como [key, value] E respondem .key/.value (as duas formas de consumo do C#), com as chaves numéricas restauradas como números (o Object.entries as transforma em string, e uma chave virada string transformaria o próximo key + 1 em concatenação). Dicionários com chave de record/struct mantêm o rebaixamento para valueMap; só chaves primitivas tomam este caminho.

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 PREGUIÇOSAMENTE

Os módulos da biblioteca compartilhada se importam por um barril só, então um campo estático inicializado no momento da avaliação do módulo pode ver a classe de outro módulo como undefined. Qualquer inicializador que nomeie outro módulo vira um getter preguiçoso respaldado por um slot privado, que também é a tradução fiel, já que o C# inicializa os statics de um tipo no primeiro uso. Literais puros continuam campos.

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#:

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

Saída TypeScript:

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

🎯 Exemplos de recursos avançados

Operações com enum

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, 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:

handleStatusChange(input: string) {
    // Parse com TryParse
    if ((status = parseEnum(input, OrderStatus), status !== undefined)) {
        console.log(`Status changed to: ${status}`);
    }

    // Pega todos os valores
    const allStatuses = Object.values(OrderStatus);
    for (const s of allStatuses) {
        console.log(`Available status: ${s}`);
    }

    // Valida
    if ((OrderStatus['Shipped'] !== undefined)) {
        console.log('Valid status');
    }
}

Operações com Dictionary

Código C#:

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:

private _settings: Record<string, number> = {};

manageSettings() {
    // Acrescenta entradas
    this._settings['timeout'] = 5000;
    this._settings['retries'] = 3;

    // Confere a existência
    if (('timeout' in this._settings)) {
        const timeout = this._settings['timeout'];
        console.log(`Timeout: ${timeout}`);
    }

    // Obtenção segura
    if ((max = this._settings['maxItems']) !== undefined) {
        console.log(`Max: ${max}`);
    }

    // Itera as chaves
    for (const key of Object.keys(this._settings)) {
        console.log(`${key} = ${this._settings[key]}`);
    }

    // Limpa tudo
    Object.keys(this._settings).forEach(k => delete this._settings[k]);
}

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);
    // 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:

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#:

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:

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#:

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:

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#:

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
    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 = null;

    // Literal default
    let number = undefined;
    this.processData(undefined);
}

processData(value = 0) {
    // value cai para 0
}

Métodos de string - exemplos adicionais

Código C#:

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:

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

    // Aparando
    const trimmed = text.trim();
    const leftTrim = text.trimStart();
    const rightTrim = text.trimEnd();

    // Conversão de caixa
    const upper = text.toUpperCase();
    const lower = text.toLowerCase();
    const upperInv = text.toUpperCase();
    const lowerInv = text.toLowerCase();

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

Clone this wiki locally