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.
🛠️ 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.
Uma implementação baseada no padrão Strategy que percorre a árvore de sintaxe (AST) do Roslyn.
•
Cada tipo de expressão ou instrução C# tem uma estratégia dedicada (ex.: BinaryExpressionStrategy, IfStatementStrategy).
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. Heurísticas de identificadores
O conversor aplica heurísticas inteligentes para decidir como mapear os nomes de variáveis:
•
Propriedades e campos C# são mapeados para this.propertyName no JS.
•
Variáveis locais e parâmetros mantêm os nomes originais.
•
Métodos do sistema como Console.WriteLine são mapeados automaticamente para console.log.
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: 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]
•
Quantificadores: Any → some/length > 0, All → every, Contains → includes
•
Agregação: Count → length/filter().length, Sum → reduce((a,b) => a+b, 0), Average → reduce()/length, 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
•
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
•
Palavra-chave default: default(int) → 0, default(string) → null, default → undefined - 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.
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.
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.
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ê.
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:
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, 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) {
if ((status = parseEnum(input, OrderStatus), status !== undefined)) {
console.log(`Status changed to: ${status}`);
const allStatuses = Object.values(OrderStatus);
for (const s of allStatuses) {
console.log(`Available status: ${s}`);
if ((OrderStatus['Shipped'] !== undefined)) {
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:
private _settings: Record<string, number> = {};
this._settings['timeout'] = 5000;
this._settings['retries'] = 3;
if (('timeout' in this._settings)) {
const timeout = this._settings['timeout'];
console.log(`Timeout: ${timeout}`);
if ((max = this._settings['maxItems']) !== undefined) {
console.log(`Max: ${max}`);
for (const key of Object.keys(this._settings)) {
console.log(`${key} = ${this._settings[key]}`);
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);
// 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);
this.processData(undefined);
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 = text.trim();
const leftTrim = text.trimStart();
const rightTrim = text.trimEnd();
const upper = text.toUpperCase();
const lower = text.toLowerCase();
const upperInv = text.toUpperCase();
const lowerInv = text.toLowerCase();
const clean = text.trim().toLowerCase().replaceAll("world", "everyone");