eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsCompilação
Matriz de recursos C# suportados
Editar esta página
76 min de leitura
Este documento traz uma lista abrangente dos recursos de C#, APIs .NET e padrões suportados pelo compilador do eQuantic.UI.
LEGENDA ✅ Suporte completo: transpila para um comportamento JavaScript equivalente. ⚠️ Suporte parcial: funciona com ressalvas ou diferenças pequenas. ❌ Sem suporte: lógica fundamentalmente incompatível (ex.: I/O bloqueante, ponteiros unsafe).
🏗️ Recursos centrais da linguagem
Recurso
Status
Notas
Classes e structs
✅
Transpilados para classes ES6. Uma classe simples tem um módulo próprio seja o que for que ela declare, inclusive uma vazia sobre os padrões de uma interface ou sobre uma base, e todo módulo que a nomeia a importa; uma classe cuja cadeia de bases chega a um atributo ou a um tipo [ServerOnly] não tem, tanto três níveis abaixo quanto um (class Underline : Mark sobre class Mark : Attribute), inclusive uma base de uma biblioteca referenciada. Uma classe de exceção da app é uma classe, com módulo próprio (ver try / catch / finally). A cadeia é lida pelo que cada base deriva e nunca pelo seu nome, então uma classe sobre uma interface com nome de atributo mantém o seu módulo, e um nome é um módulo onde qualquer declaração dele for um. Uma classe, uma classe estática, um record ou uma struct declarados dentro de outro tipo também são um módulo próprio, com o nome dos tipos que os contêm e o seu, unidos por $ (Cart$Item, A$B$C), que nenhum tipo de topo pode ter: toda referência a ele, dentro do dono ou fora, nomeia esse twin, então uma classe aninhada ao lado de uma classe de topo com o seu nome nunca é tomada por ela, e uma dentro de uma classe só do servidor ou de um atributo não tem nenhum, como o dono não tem: código do cliente que a nomeia é recusado (EQ2010), e uma página declarada nela não tem rota, enquanto uma dentro de uma classe de exceção atravessa como essa classe (Desde 0.2.0-preview.61). Um record e uma struct têm twin seja o que for que declarem, a não ser que estejam marcados [ServerOnly], inclusive um record sobre um record server-only (Desde 0.2.0-preview.61).
Interfaces
✅
Usadas na checagem de tipos do TypeScript (apagadas em runtime). Um membro padrão (uma propriedade, um indexador ou um método com corpo; um indexador desde 0.2.0-preview.61) chega ao twin de toda classe que o herda sem declará-lo: convertido do código-fonte da interface, ou, para as interfaces do próprio SDK que uma app implementa (um tema, uma linguagem de código), delegado à cópia do runtime. A interface de qualquer outro assembly compilado não tem nem um nem outro, e a build recusa a classe (EQ1008) (Desde 0.2.0-preview.59).
Enums
✅
Atravessam como o nome em camelCase (MainAlign.Start → 'start'), e um enum [Flags] como o seu número. .ToString(), interpolação e concatenação dão o nome do membro C# ("Start"), o mesmo texto que o servidor imprime, 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", o formato e o alinhamento de uma interpolação escrevem o que o .NET escreve, Enum.Parse, TryParse, GetName, GetNames, GetValues e IsDefined respondem como os do .NET, e um dicionário com chave de enum atravessa nos dois sentidos, recusando um nome que nenhum membro tem. Desde 0.2.0-preview.15; os formatos, o texto de flags, os estáticos de Enum e as chaves de enum desde 0.2.0-preview.60.
Genéricos
✅
Totalmente suportados (apagados em runtime).
Extension methods
✅
Resolvidos pelo Semantic Model e transpilados para chamadas diretas.
Async / await
✅
Mapeiam para async / await e Promise.
Expressões lambda
✅
Mapeiam para arrow functions () => {}.
Pattern matching
✅
Padrões is, padrões de propriedade, padrões recursivos. Um tipo sem nada ligado (o is int or long, int => …, case int:) testa o tipo, um long como BigInt, um inteiro como número inteiro e um char como uma string de uma unidade de código. Uma constante num padrão, x is Limits.Max inclusive, é testada pelo seu valor: um null casa com qualquer ausência, um decimal e um NaN comparam por valor, e uma constante long é um BigInt, como todo long (Desde 0.2.0-preview.60). Um padrão posicional sobre um tipo cujo Deconstruct o app escreveu, o de uma extensão inclusive, chama-o uma única vez por valor em cada teste, mesmo entre os braços de um switch e as alternativas de um or, como o .NET, e lê as partes que ele devolve, como uma desconstrução faz: lia os membros com o nome dos seus out, que uma parte calculada pelo Deconstruct não é (Desde 0.2.0-preview.61).
Interpolação de string
✅
Mapeia para template literals ${var} . Um formato com texto entre aspas ({date:dd 'de' MMMM}) mantém as aspas, e uma raw string interpolada mantém as chaves ($$$"""{{x}}""" é {{x}}) (Desde 0.2.0-preview.60). Um alinhamento é o número que é, inclusive uma constante pelo nome ou uma expressão de constantes ({name,Widths.Name}, {name,-(Widths.Name + 1)}), num StringBuilder também (Desde 0.2.0-preview.62).
Literais de string e char
✅
Uma string ou um char mantém o valor que o C# lê, seja qual for a grafia: um surrogate isolado ("x\uD83D"), um caractere de controle e os escapes que só o C# tem ('\a', '\e', '\x041', '\U00000041'). Um literal UTF-8 ("ab"u8) não tem gêmeo e falha a build (EQ1004) (Desde 0.2.0-preview.60).
Null-coalescing
✅
?? e ??= mapeados para os equivalentes JS.
Inicializadores de objeto
✅
new Obj { Prop = 1 } sobre um record, uma struct, uma classe comum, uma exceção ou um tipo do vocabulário cujo twin o runtime transpila (os modelos da planilha e dos formulários, CellRef, FieldError, SheetController…), aplicado quando o construtor já voltou, como o C# o aplica (antes da 0.2.0-preview.61, uma classe comum o recebia como um objeto de config no fim, uma exceção o descartava, e um tipo transpilado do vocabulário o entregava a uma posição que o seu twin não tem; um componente o recebe como as suas props): todo inicializador do tipo já rodou, inclusive o do próprio membro, e os valores são lidos depois deles. Um inicializador de coleção aninhado (Items = { 1, 2 }) adiciona à coleção que o membro guarda, pelo Add que a árvore vinculada liga (o de um método de extensão pela sua casa, e o de uma lista, de um conjunto ou de um membro ICollection<T> como uma chamada a ele é); um inicializador de objeto aninhado (Inner = { N = 3 }) atribui dentro do objeto que o membro guarda; e uma entrada ([key] = value) é escrita pelo indexador do tipo. O par de um dicionário (Map = { { "a", 1 } }) é o seu Add, que recusa uma chave que já está lá. Cada parte é avaliada no método em que foi escrita, então um await num elemento funciona, e cada elemento é aplicado antes de as partes do seguinte serem avaliadas, o membro a que ele adiciona ou atribui lido antes das suas próprias partes, como o C# o aplica. Um elemento sem conversão faz a build falhar (EQ1004) (Desde 0.2.0-preview.61).
field (C# 14)
✅
Propriedades guardadas: o gêmeo ganha um slot $name (um nome que nenhum campo C# pode ter), um getter e o corpo do próprio acessor. Uma estática guarda o slot no tipo, a de um record inclusive, e o slot começa com o inicializador da propriedade ou com o default do seu tipo (Desde 0.2.0-preview.60). A de instância de um record ou de uma struct roda os corpos dos seus acessores sobre o slot, como a de uma classe, e uma cujo getter tem corpo é estado que o record compara e copia; antes da 0.2.0-preview.61 ela era guardada sob o nome da propriedade, e o corpo do setter se perdia (Desde 0.2.0-preview.61).
Propriedades virtuais e override
✅
Uma propriedade que um tipo derivado pode sobrescrever ou que sobrescreve outra (uma auto-propriedade declarada virtual ou override) guarda o valor numa loja própria ($name), sob acessores no protótipo do gêmeo, numa classe, num record e numa struct igualmente, então toda leitura chega ao override e nenhum inicializador chega ao acessor de outro nível: uma auto-propriedade sobre uma calculada, ou uma calculada sobre uma auto-propriedade, lançava no new ou respondia o valor da base antes da 0.2.0-preview.61. Um override que declara só o getter de uma propriedade cuja base tem os dois mantém o setter da base, como o C# o herda. Um record compara, combina no hash e copia a loja, como o .NET compara um campo de apoio. Um gêmeo com loja a escreve no JSON sob o nome da propriedade, então uma server action recebe a propriedade pelo nome C#. Uma diferença fica: num tipo que sobrescreve uma auto-propriedade com outra, base.Name lê o valor do override, onde o .NET lê o campo da própria base, já que as duas compartilham uma loja (Desde 0.2.0-preview.61).
Membros que ocultam (new)
✅
Um método que oculta um membro herdado, com new ou com a mesma assinatura e sem override, é alcançado como o C# o alcança: pelo tipo base responde o método da base, e pelo tipo que oculta, o que oculta, numa chamada, num method group, numa chamada base e num ?. igualmente, um método genérico sobre o seu próprio parâmetro de tipo incluído, e o mesmo vale para um membro que o runtime lê pelo seu nome .NET, então ((object)c).GetHashCode() é o de object sobre um public new int GetHashCode(). O gêmeo o guarda sob um nome próprio (name$1), e um override de um método new virtual ocupa o lugar desse método. O mesmo vale para um método ao lado de uma implementação explícita de interface do seu nome, então uma chamada pela classe chega a ele e uma pela interface à explícita. Um que oculta um membro, ou fica ao lado de uma implementação assim, e responde a uma interface falha o build (EQ1007), e uma propriedade ou um campo que oculta outro ainda divide o nome com ele (#563, desde 0.2.0-preview.61).
Acessores init
✅
Emitidos como setter JS, porque um corpo de init é onde um tipo declara a invariante dele, e descartá-lo perderia a checagem no cliente.
Inicializadores de coleção
✅
new List<int> { 1, 2 } mapeia para [1, 2]. Uma lista construída com um argumento e um inicializador guarda os dois, como o C# a constrói: uma cópia da fonte, nunca a própria fonte, ou nada para uma capacidade, e depois cada elemento em ordem, a fonte ou a capacidade lida primeiro. Uma capacidade ainda é avaliada, e uma negativa é recusada como o construtor a recusa. Assim new List<int>(source) { 3 } é [...source, 3] e new List<int>(10) { 1, 2 } é [1, 2], escrita por extenso ou com o tipo do alvo, e List<int> c = new(source) é uma cópia. Uma capacidade descartava os elementos, e uma fonte era juntada a eles por uma vírgula, antes da 0.2.0-preview.61 (Desde 0.2.0-preview.61).
IEnumerable<T> próprio
✅
Uma classe, um record ou uma struct que implementa IEnumerable<T>, ou só IEnumerable, é enumerada como o seu próprio GetEnumerator() diz, por foreach, um spread, string.Join e todo operador LINQ, cada um pelo que o C# liga para ele: um foreach sobre a classe percorre o seu próprio GetEnumerator() público (o de um enumerador struct incluído), e LINQ, um spread, string.Join e um foreach sobre a interface percorrem o de IEnumerable<T>, explícito ou não, então os dois podem percorrer sequências diferentes, como no .NET. É a sequência que um método iterador produz, ou um enumerador seu, uma classe que é o próprio enumerador incluída, percorrido pelo seu MoveNext e Current e descartado quando o percurso acaba, inclusive por um break. O override de uma classe derivada responde, o IEnumerable.GetEnumerator() explícito ao lado do genérico é a mesma sequência, e também um Current explícito ao lado de um público. Uma classe só com um GetEnumerator() público também é percorrida pelo foreach. O gêmeo tem uma iteração só e um Current só, então um tipo que implementa IEnumerable<T>, ou IEnumerator<T>, sobre dois tipos falha o build (EQ1007), e também um IEnumerable.GetEnumerator() ou um Current explícito que responde de outro modo que o membro que o gêmeo mantém, onde um que devolve a resposta dele (=> GetEnumerator();, => Current;) é guardado como um só. O GetEnumerator() de uma coleção do .NET (=> items.GetEnumerator()) não tem tradução e falha o build (EQ2004): produza os itens com yield (#612, desde 0.2.0-preview.61).
Desconstrução
✅
var (a, b) = tuple mapeia para [a, b] = tuple, e também (var c, var d) = … e designações aninhadas. Um record ou um struct se desconstrói pelo Deconstruct que chama, em toda forma: uma declaração, uma atribuição ((a, b) = point, (var a, b) = point), um foreach (var (a, b) in points) e uma desconstrução aninhada. O de um record lê os seus membros, e um Deconstruct escrito pela app é executado. Os receptores e os índices dos alvos são avaliados antes do valor, como o C# os avalia, então (xs[i++], xs[i++]) = point escreve os elementos que o C# escreve (Desde 0.2.0-preview.60). Cada parte chega ao seu alvo como o C# a põe lá: convertida para o tipo do alvo (uma parte int num long), e escrita pelo que o alvo é, a entrada de um dicionário pela sua classe, numa atribuição, numa declaração e num laço (Desde 0.2.0-preview.60).
Variáveis de expressão
✅
O que um padrão, um out var ou uma desconstrução declara é declarado onde o C# declara, em todo tipo de membro: no bloco que os contém depois de um if ou de uma instrução de expressão, dentro de um laço ou de um using, e novo a cada iteração e a cada chamada, então um closure guarda o próprio valor. Um nome que o JavaScript reserva (@class) é renomeado uma vez, na declaração e em toda referência (Desde 0.2.0-preview.59).
Funções locais
✅
Transpiladas para funções internas. Um parâmetro out ou ref leva o seu valor de volta a quem chama, como o de um método (Desde 0.2.0-preview.60).
Laços for
✅
1:1. A variável de um laço é uma por laço, como no C#: um closure criado no corpo lê o valor atual dela, nunca uma cópia da própria iteração, e o mesmo vale para o que o inicializador declara (out var n, var (i, j) = (0, 3)), um cabeçalho de expressões incluído (for (Seed(out var n); …)) (Desde 0.2.0-preview.60).
Delegates
✅
Chamados como valores, pelo nome, por um membro ou a partir de qualquer expressão (handlers[0](x), Make()(x), desde 0.2.0-preview.60).
Records (with)
✅
Uma cópia do record na sua própria classe, e depois os membros que a expressão nomeia: como a cópia do .NET, não roda inicializador nem construtor. O with de uma struct, e o dos records do motor de código, passam pela cópia do próprio twin. Um record cuja cadeia declara um construtor de cópia é copiado por ele, como o with do C#: um declarado começa os seus membros no zero, roda a cópia da base com o que o : base(…) passa e depois o seu corpo, e um nível sintetizado copia os próprios membros (Desde 0.2.0-preview.61).
Construtores
✅
O construtor do twin de um record, de uma struct ou de uma classe comum é o construtor C# que a chamada liga, cada um alcançado pelas quantidades de argumentos que aceita, contando os parâmetros opcionais (antes da 0.2.0-preview.61, uma classe comum ficava só com o mais largo). Um que faz o próprio trabalho (o primário, ou um explícito que não encadeia) liga os seus parâmetros, roda todo inicializador na ordem da declaração, o de um campo e o de uma propriedade igualmente, os de um tipo derivado antes do construtor da base, chama o construtor da base com os seus próprios argumentos e roda o seu corpo; um que encadeia com : this(…) roda aquele em que encadeia e depois o próprio corpo, mesmo quando o corpo daquele retorna cedo. Cada argumento cai no lugar do seu parâmetro e é avaliado na ordem em que está escrito, em new, : this(…), : base(…) e numa cláusula de base: um nomeado, um opcional omitido, e um array params passado inteiro (nomeado ou não) ou formado pelos seus elementos; uma variável que um argumento declara (out var n) é declarada onde ele é avaliado, e o parâmetro de um construtor é uma variável que o seu corpo pode atribuir. O construtor de cópia de um record não é um ramo. Dois construtores que aceitam a mesma quantidade de argumentos fazem a build falhar (EQ1009), e também um encadeamento num construtor que encadeia por sua vez e um : this() para o construtor implícito de uma struct ao lado de construtores próprios. O zero de uma struct (default, a posição de um array, new S() sem construtor próprio, inclusive o de uma struct genérica, que guarda o zero de cada argumento de tipo, e o de uma de outro assembly) não roda nada disso, nem o construtor estático. O construtor primário de uma classe comum liga os seus parâmetros como o de um record, e um que um membro lê fica guardado em cada instância com o seu nome, então um que cai no nome de outro membro faz a build falhar (EQ1007). Uma diferença fica: o construtor de uma base que lê um membro de uma classe derivada por um membro virtual o lê sem valor, onde o .NET o lê inicializado, porque o JavaScript não deixa nada tocar a instância antes de o super() voltar (Desde 0.2.0-preview.61).
Indexadores
✅
Um indexador de instância é o item(…) e o setItem(…, value) do seu twin, e todo acesso a ele os chama: uma leitura, uma escrita, uma atribuição composta, ++ num valor de qualquer tipo, ??=, ?[…], o alvo de uma desconstrução, uma chave a partir do fim (^1, na contagem que o seu tipo nomeia) e a entrada de um inicializador de objeto, com o receptor e as chaves avaliados uma vez cada, cada chave no lugar do seu parâmetro (uma chave nomeada, uma opcional omitida, uma params). Uma atribuição responde o valor que atribuiu, seja o que for que o setter faça com a sua cópia, e uma implementação explícita do indexador de uma interface responde aos acessos pela interface. Um segundo indexador num tipo, ou qualquer membro em Item ou SetItem ao lado de um, faz a build falhar (EQ1007) (Desde 0.2.0-preview.61). Um acesso por uma interface de lista (IList<T>, IReadOnlyList<T>, IList) alcança o que a interface guarda quando roda, o elemento de um array ou de uma lista e o item e o setItem de um twin igualmente, em cada uma dessas formas, e o seu Count conta um tipo seu pelo próprio Count, nunca por um Length ao lado dele: a interface lia um subscrito e um length, que um twin não responde, antes da 0.2.0-preview.61 (Desde 0.2.0-preview.61).
Intervalos
✅
x[a..b] sobre uma string, um array ou uma lista é o slice do JavaScript, e um extremo que pode ser ^0 se resolve contra o comprimento como o Index.GetOffset faz. Sobre um tipo seu com um Length (ou um Count) e um Slice(int start, int length), chama esse Slice como o C# rebaixa o intervalo: o receptor uma vez, depois os extremos na ordem em que estão escritos, depois a contagem, lida só onde um extremo conta a partir do fim ou o fim fica aberto. Assim strip[1..3] é Slice(1, 2), onde chamava o slice(1, 3) do twin antes da 0.2.0-preview.61. Um intervalo passado a um indexador que recebe o próprio Range faz a build falhar (EQ2004), como um Range guardado como valor faz (Desde 0.2.0-preview.61).
Inicialização estática
✅
Os estáticos de um tipo começam no seu zero, depois os inicializadores rodam na ordem da declaração, depois o construtor estático, uma vez, na primeira vez que um deles é lido ou escrito: static int A = B + 1; static int B = 2; deixa A em 1, como no .NET, num record, numa struct, numa classe, numa classe estática e num componente. Um tipo com construtor estático o roda antes da sua primeira instância e do primeiro uso de qualquer um dos seus membros estáticos, inclusive um método, uma propriedade calculada e um operador. Os inicializadores rodam na ordem da declaração entre campos, propriedades e eventos de campo, e o corpo do construtor estático depois deles, com um return nele terminando só ele; um que lança faz todo uso do tipo lançar uma TypeInitializationException, como o .NET faz; um estático cujo inicializador o C# reduz a uma constante é o seu valor; e o construtor estático de um componente roda uma vez (Desde 0.2.0-preview.61).
typeof
✅
Mapeia para o literal de string com o nome do tipo.
Tipos base
✅
Uma classe estende o gêmeo da base como quer que a base esteja escrita: sem qualificação, com o namespace, por um alias ou com global:: (Desde 0.2.0-preview.60).
Nomes de tipo qualificados
✅
Um tipo alcançado pelo seu namespace, por parte dele, por ele inteiro ou por global:: é importado como um tipo alcançado por um using: dentro de App.Chat, Portal.Fold.Text(n) responde o que Fold.Text(n) responde com using App.Portal; (Desde 0.2.0-preview.61).
Chamadas base
✅
Mapeiam para a palavra-chave super.
using static
✅
Um membro alcançado pelo nome simples responde o que a forma qualificada responde: PI é Math.PI, Join(…) é string.Join(…), o High de um enum é Level.High, e Now sob using static System.DateTime é DateTime.Now (Desde 0.2.0-preview.61; até então um membro do .NET fora de Math, MathF, string, uma constante e um enum falhava o build mesmo onde a forma qualificada tinha tradução). Um membro do .NET cuja forma qualificada também não tem tradução falha o build (EQ2004) em vez de chegar ao browser (Desde 0.2.0-preview.60).
O Count de um tipo
✅
Um tipo que a app ou uma biblioteca que ela referencia declara lê o próprio Count: o modelo de domínio de uma biblioteca o lia como o length de um array, undefined (Desde 0.2.0-preview.60).
cast e as
✅
Mapeiam para passagem/truncamento em JS.
sizeof
✅
Mapeia para os tamanhos dos primitivos C#.
Métodos anônimos
✅
delegate(...) { ... } mapeia para arrow functions.
params
✅
Um parâmetro REST (...xs). As duas formas de chamada do C# funcionam: argumentos expandidos e um array passado inteiro, que é espalhado. Desde 0.2.0-preview.15.
stackalloc
✅
Mapeia para typed arrays (ex.: Int32Array).
yield return
✅
Mapeia para generator functions do JS (function*).
try / catch / finally
✅
As cláusulas catch são UM catch JavaScript que as tenta na ordem do C#: cada uma pelo seu tipo de exceção, um tipo derivado dele incluído, e pelo seu filtro when, que declara as próprias variáveis (out var n, um padrão) e responde false onde lança, como o do .NET. Uma exceção que nenhuma cláusula pega segue adiante, e throw; relança a exceção capturada. Uma exceção é um Error JavaScript que carrega os seus tipos .NET, uma classe de exceção da própria app e Exception e = new("…") incluídas, então e is ArgumentException, um switch sobre exceções e as também testam o tipo, e uma exceção que o runtime lança é do tipo que o .NET lança ali (um estouro, uma chave ausente, um formato inválido, um cast recusado); um TypeError do JavaScript é uma NullReferenceException. Uma diferença fica, a da plataforma: o .NET roda um filtro ANTES dos blocos finally entre o throw e a cláusula, e o JavaScript depois deles. Desde 0.2.0-preview.60. A mensagem de uma exceção do framework é composta como o .NET a compõe: quando nenhuma mensagem ou uma nula foi dada, o texto que o .NET escreve para aquele construtor, lido do próprio .NET, depois (Parameter 'x'), e o valor real e o nome de um objeto descartado em linhas próprias, as mensagens internas de uma agregada depois da sua e a frase de um inicializador de tipo, e ParamName, ActualValue, InnerException e TypeName leem o que o construtor recebeu, o InnerException de uma agregada sendo a primeira interna, as exceções de argumento do próprio runtime nomeando o seu parâmetro também (Desde 0.2.0-preview.61). Um argumento nulo é recusado como o .NET o recusa, uma ArgumentNullException que nomeia o parâmetro, pelos Max, Min e ToDictionary do LINQ, por um operador LINQ que recebe uma sequência que não é uma lista (o second do Concat, o first do Zip), por new Guid(text), char.GetUnicodeCategory(text, i), CancellationToken.Register e CreateLinkedTokenSource, e CompareTo(object) responde 1 para um nulo, como o do .NET. Um membro traduzido para o método do próprio JavaScript, o LINQ sobre uma lista entre eles, ainda lê um nulo através do nulo, uma NullReferenceException: a suíte de conformidade chama cada membro da superfície traduzida com um nulo em cada parâmetro de referência, e lista esses em null-argument-gaps.baseline.txt com as suas razões (Desde 0.2.0-preview.61). Uma classe de exceção da app é uma classe sobre a base de exceção do runtime, o Error que carrega os seus tipos: os seus campos, as suas propriedades, os seus construtores e os seus métodos são dela, e a sua chamada à base entrega o que um new da sua base do .NET entrega, então : base(message, inner) chega a Message e a InnerException, : base("bad", name) sobre ArgumentException lê bad (Parameter 'x') e o seu ParamName, uma classe sobre InvalidOperationException que não dá mensagem lê o texto próprio daquele tipo, uma mensagem ausente sobre Exception é o padrão do .NET (Exception of type 'App.Failure' was thrown.), um membro dela sob um nome que a base guarda (um Name, um override de Message ou de ParamName) responde por ele, e uma genérica é capturada pela sua construção (catch (Failed<int>)) desde que o seu construtor roda, um throw this ali incluído. Uma mensagem ausente nomeia o tipo como o .NET o nomeia em execução (App.Failed1[System.Int32], App.Outer+Inner). Ela é transpilada como qualquer classe: uma que o browser nunca vê, cujos membros chegam ao que ele não consegue rodar, é marcada [ServerOnly], e dois construtores dela que aceitam a mesma quantidade de argumentos são recusados (EQ1009), menos o que só a serialização do .NET chama, (SerializationInfo, StreamingContext), que o template do Visual Studio escreve e o gêmeo deixa de fora, e uma chamada desse que o browser faria, um new ou a chamada à base de outro construtor, falha o build (EQ1009). Uma exceção construída ou testada onde um argumento de tipo que ela carrega é um parâmetro de tipo também falha o build (EQ2014), já que o JavaScript o apaga: new Failed<T>() num método genérico, catch (Failed<T>), new Missing() dentro da classe genérica que a declara. Uma declarada dentro de outro tipo também é uma classe, com o gêmeo nomeado pelo dono (Holder$Nested), e o que uma classe de exceção declara também, um record do motivo que ela carrega (Rejected$Reason). Num trecho compilado sozinho, sem projeto de onde ler as suas bases, uma classe de exceção da app ainda é o Error` do runtime com os seus tipos e nenhum dos seus membros (#611, desde 0.2.0-preview.61).
Expressões throw
✅
x ?? throw …, c ? a : throw … e => throw …, a exceção avaliada só onde é lançada, um await nela incluído. Desde 0.2.0-preview.60.
lock
⚠️
Transpilado só para o corpo (JS é single-threaded). A expressão roda uma vez, na frente dele, a menos que só leia uma variável, um campo ou this (Desde 0.2.0-preview.60; a que declara uma variável, desde 0.2.0-preview.59). new object(), o portão de costume, é um objeto novo, com ou sem um inicializador vazio.
🔄 Suporte a LINQ
O compilador inclui estratégias especializadas para quase todos os métodos LINQ.
Lógica
Métodos
Status
Filtragem
Where, OfType
✅
Projeção
Select, SelectMany, Cast
✅
Particionamento
Skip, Take, SkipWhile, TakeWhile
✅
Ordenação
OrderBy, OrderByDescending, ThenBy, Reverse
✅
Agregação
Count, Sum, Min, Max, Average, Aggregate
✅
Quantificadores
Any, All, Contains
✅
Conjuntos
Distinct, DistinctBy, Union, Intersect, Except, Concat
✅
Elementos
First, FirstOrDefault, Single, Last, ElementAt
✅
Utilitários
SequenceEqual, DefaultIfEmpty
✅
Geração
Enumerable.Range, Enumerable.Repeat, Enumerable.Empty
✅
Conversão
ToList, ToArray, ToDictionary, ToHashSet
✅
Agrupamento/Join
GroupBy, Join, Zip
✅
Todo operador lê a sua fonte como a sequência que o .NET enumera, seja como for que o navegador a guarde: um array ou uma lista como são, um HashSet, os pares de um dicionário e o sorted set, o sorted dictionary, a sorted list, a fila, a pilha e a lista ligada do runtime pela própria enumeração (uma pilha a partir do topo), e uma string pelos seus chars, as unidades de código UTF-16. ToList e ToArray sempre criam um array novo, então uma cópia e a sua fonte são duas listas. O foreach e o ToCharArray de uma string também dão unidades de código, um par substituto sendo dois chars, new string(chars) e new string(chars, start, length) constroem o seu texto, e um método estático de char passado como delegate funciona, "a1b2".Count(char.IsDigit) e Count(IsDigit) sob using static System.Char igualmente (Desde 0.2.0-preview.60).
Enumerable.Range e Repeat avaliam os seus argumentos uma vez, onde o C# avalia, um await entre eles incluído: Range(Start(), 3) chama Start uma vez, e Repeat(new List<int>(), 3) repete uma lista só. Uma contagem negativa, ou um Range além de int.MaxValue, lança a ArgumentOutOfRangeException do .NET (Desde 0.2.0-preview.60).
Um comparador passado a ToDictionary, ToLookup, GroupBy, Distinct, ToHashSet ou SequenceEqual passa pela cerca por que passa o construtor de uma coleção. Um que pede a igualdade padrão do tipo da chave (EqualityComparer<T>.Default, um comparador nulo, ou StringComparer.Ordinal para uma string) responde como o operador responde sem ele, cada seletor no seu parâmetro, nomeado ou não, e qualquer outro, como StringComparer.OrdinalIgnoreCase, faz a build falhar com EQ2007. ToDictionary e GroupBy recusavam todo comparador, ToLookup chamava um como seletor de elemento e Distinct descartava o que recebesse, antes da 0.2.0-preview.61 (Desde 0.2.0-preview.61). SequenceEqual respondia a sua primeira sequência quando recebia um comparador, antes da 0.2.0-preview.62 (Desde 0.2.0-preview.62).
Cada sobrecarga responde como a sua própria sobrecarga do .NET, cada argumento no parâmetro a que se liga, nomeado ou não. FirstOrDefault(defaultValue), LastOrDefault(defaultValue) e SingleOrDefault(defaultValue), com predicado ou sem, respondem o elemento quando há um, um elemento nulo incluído, e o valor passado caso contrário, e SingleOrDefault recusa um segundo elemento ou uma segunda correspondência com a InvalidOperationException do .NET. Zip(second) responde pares e Zip(second, third) triplas, lidas como First, Second e Third ou desconstruídas, tantas quantas a sequência mais curta tiver. Average calcula a média no tipo do elemento: um int ou um long somado exatamente num long, uma OverflowException além de long.MaxValue, e dividido como double, uma média de float arredondada para single, uma de decimal como decimal, um anulável só sobre os seus valores e nulo quando não tem nenhum, e uma sequência vazia recusada com uma InvalidOperationException onde a resposta não é anulável. SequenceEqual compara pela igualdade padrão do tipo do elemento: um long e um decimal pelo valor, um record pelos seus membros, uma classe pelo seu próprio Equals e pela referência quando não tem um, um array pela referência, e NaN igual a si mesmo. Antes da 0.2.0-preview.62 um valor padrão era chamado como predicado, a terceira sequência de um Zip como seletor de resultado, e uma média de longs lançava um TypeError (Desde 0.2.0-preview.62).
📦 Tipos .NET (BCL)
Mapeamos os tipos .NET comuns para os equivalentes em JavaScript.
Primitivos
Tipo .NET
Equivalente JavaScript
string
String
int, double, float
Number
bool
Boolean
object
Object
dynamic
any
System.String
•
✅ Join, Format. O string.Join lê qualquer sequência (um array, uma lista, um HashSet, uma LinkedList, as chaves de um dicionário, uma sequência atrás de uma interface) e escreve cada valor como o ToString do .NET escreve: um bool como True, um enum pelo nome do membro, um float pelos próprios dígitos, um double na notação do .NET (1E+21), um null como nada; os valores passados um a um são escritos cada um pelo seu tipo, e um separador null não escreve nada (Desde 0.2.0-preview.61)
•
✅ IsNullOrEmpty, IsNullOrWhiteSpace
•
✅ Split, Replace, Substring, Trim. Split responde como a sobrecarga do .NET: um conjunto de separadores (um array de chars ou de strings, ou chars escritos um a um) divide em qualquer um deles, o primeiro na ordem do conjunto vencendo numa posição; um separador único nulo ou vazio não divide nada, e um conjunto nulo ou vazio divide no espaço em branco do .NET; a contagem limita as entradas, a última ficando com o resto; TrimEntries e RemoveEmptyEntries se aplicam, escritos por extenso ou guardados numa variável; e uma contagem negativa ou uma opção indefinida lança a exceção do .NET (Desde 0.2.0-preview.62)
•
✅ ToLower, ToUpper e o seu par invariante mudam a caixa como o .NET 10 muda: cada unidade de código, ou par substituto, vira exatamente uma outra (ß e fi ficam como estão, ᾳ em maiúscula é ᾼ, e um sigma maiúsculo em minúscula é σ onde quer que esteja), e os dois i's são os da cultura: tr e az passam i a İ em maiúscula e I a ı em minúscula, toda outra cultura passa ı a I e İ a i, e a cultura invariante deixa İ e ı como estão. Uma letra cuja maiúscula veio com o Unicode 16, como ƛ, mantém a caixa, como o .NET 10 a mantém num servidor Linux ou Windows, cujo ICU é anterior; o .NET no macOS 26 muda a sua caixa, pelo ICU da Apple. Um argumento de cultura atravessa como CultureInfo.CurrentCulture ou um nulo, que são a cultura da página, e CultureInfo.InvariantCulture; qualquer outra faz a build falhar (EQ2108) (Desde 0.2.0-preview.62, quando o navegador alongava ß para SS, lia um sigma final pelo seu contexto, não conhecia cultura nenhuma e descartava a que a chamada nomeava)
•
✅ StartsWith, EndsWith
•
✅ Equals, StartsWith, EndsWith, Contains, IndexOf, LastIndexOf e Replace com um StringComparison, por essa comparação, e CompareTo pela cultura atual; uma busca por uma comparação de cultura falha o build (EQ1004) (Desde 0.2.0-preview.60)
System.DateTime, TimeSpan, DateOnly, TimeOnly e DateTimeOffset
Os tipos temporais são respaldados por tipos compatíveis com precisão de tick (ticks de 100 ns, calendário gregoriano proléptico, não o mapeamento com perda de new Date() / milissegundos numéricos). Construtores, componentes, Add*, aritmética (-→TimeSpan), comparações e o .ToString() batem todos com o .NET, o texto de uma data escrito por um só formatador na cultura em vigor (Desde 0.2.0-preview.61). Um TimeSpan escreve o formato que recebe, pelo ToString, por um buraco de interpolação e por um marcador de composição: c, t e T a sua forma constante, g e G as gerais, com o separador decimal da cultura, um formato personalizado como hh\:mm token a token, e um formato que o .NET recusa a sua FormatException nas palavras do .NET (Desde 0.2.0-preview.62, quando todo formato escrevia a forma constante); os valores atravessam o fio do SSR como strings ISO-8601 / "c" e são hidratados de volta no tipo compatível. TryParse(text, out var value) de cada um dos cinco responde true com o valor que o seu Parse lê, ou false com o padrão do tipo em value para um texto que o Parse recusa e para um nulo, como o do .NET (Desde 0.2.0-preview.62, quando quatro deles derrubavam a página e o do DateOnly respondia null). Parse e TryParse leem na cultura que a chamada nomeia, como o .NET: sem provedor, com CultureInfo.CurrentCulture ou com um nulo, a cultura da página, e com CultureInfo.InvariantCulture, a invariante. Eles leem as formas numéricas que a cultura escreve: uma data de três números na ordem do seu padrão de data curta (01/02/2024 é 1º de fevereiro em pt-BR e 2 de janeiro em en-US), os seus separadores ao lado de /, - e ., um relógio com o seu próprio separador (o 21.30 do fi-FI, onde três números curtos unidos por pontos são um relógio e 10.11.12 é 10:11:12) e os seus próprios designadores de AM e PM ao lado de AM e PM, antes ou depois do relógio, o relógio antes ou depois da data, uma fração depois dos segundos com ponto ou vírgula, um fuso, e a forma ISO (2024-01-15T09:30:00Z). Um TimeSpan é lido como o .NET lê um sem formato: por quantos números ele tem e pelo que está entre eles, contra o padrão invariante d.hh:mm:ss.fffffff e o da cultura, d:hh:mm:ss e o seu separador decimal, então 10 são dez dias, 1:2 uma hora e dois minutos, 1:02:03:04 um dia e duas horas, e 24:00:00, que nenhum relógio lê, vinte e quatro dias; uma parte além do seu limite ou um intervalo além do alcance do tipo lança a OverflowException do .NET com as suas palavras, e uma fração com mais de sete dígitos é arredondada como o .NET arredonda (Desde 0.2.0-preview.62, quando só [-][d.]hh:mm:ss[.f] era lido e nenhuma parte era verificada, então 00:60:00 era uma hora). Assim um valor escrito com ToString() é lido de volta, e o que é recusado é recusado nas palavras do .NET. Qualquer outra cultura falha o build (EQ2108), e também estilos que não sejam espaço em branco e um span (EQ1004); ParseExact, TryParseExact e GetDateTimeFormats não são traduzidos (EQ2004). Convert.ToDateTime lê um texto como o DateTime.Parse lê, um texto nulo como MinValue, um objeto pelo que ele contém e uma data como ela mesma, e um valor de outro tipo lança a InvalidCastException do .NET; um char guardado num objeto é lido como um texto de um caractere, porque um valor encaixotado não leva o seu tipo (#552) (Desde 0.2.0-preview.62, quando todo leitor lia as formas invariantes, um provedor derrubava a página e o Convert.ToDateTime respondia o texto). O nome de um mês, uma data de dois números, um relógio sem data e calendários que não sejam o gregoriano ainda não são lidos (#807). Um construtor que recebe um Calendar faz a build falhar (EQ1004), porque o navegador não tem nenhum dos calendários do .NET, o do DateOnly incluído (Desde 0.2.0-preview.62, quando ele descartava o calendário). Veja a tabela Cobertura e conformidade da BCL .NET abaixo para o detalhe por tipo.
System.Collections.Generic
•
✅ List<T> → Array [] do JavaScript; Remove responde o bool que o .NET responde e compara como o EqualityComparer<T>.Default compara, um record pelo valor, uma tupla elemento a elemento e NaN com NaN (Desde 0.2.0-preview.59). IndexOf, LastIndexOf, Contains e Remove, Array.IndexOf e Array.LastIndexOf, e o Contains do LINQ comparam por essa única comparação, escolhida pelo tipo do elemento: um NaN, um record, uma tupla e um decimal encontrados pelo valor, uma classe pelo seu próprio Equals, o membro array de uma tupla pela referência, como o ValueTuple.Equals compara. Sort e Array.Sort são a ordenação introspectiva do .NET, instável como a do .NET, então elementos iguais ficam onde os do .NET ficam, pelo comparador padrão do tipo (os NaN de um double primeiro), um Comparison<T>, os seis do StringComparer, Comparer<T>.Create ou um comparador seu, com as exceções do .NET; qualquer outro comparador falha o build (EQ2007). BinarySearch responde o complemento do ponto de inserção quando não acha, Find e FindLast o default do tipo do elemento, RemoveAll quantos removeu, CopyTo escreve no array que recebe, e os intervalos de FindIndex, FindLastIndex, IndexOf e LastIndexOf são verificados com as palavras do .NET (Desde 0.2.0-preview.61)
•
✅ O Add e o Clear de ICollection<T> acrescentam e esvaziam como a coleção por trás da interface faz quando a chamada roda: uma lista acrescenta no fim, um set acrescenta um valor que não tem, uma lista ligada acrescenta por último, um dicionário acrescenta o par e recusa uma chave que já está lá com as palavras do .NET, e um tipo seu roda o próprio Add e o próprio Clear, o inicializador de coleção de um membro (Items = { 1, 2 }) incluído. Eram o push e o splice de um array, que um set, uma lista ligada e um dicionário não têm, antes da 0.2.0-preview.61 (Desde 0.2.0-preview.61)
•
✅ Dictionary<TKey, TValue>, IDictionary e IReadOnlyDictionary → a classe de dicionário do runtime ($eq.collections.dictionary), que guarda as entradas por posição como a do .NET: enumera na ordem de inserção, e uma entrada adicionada depois de uma remoção ocupa o lugar da removida, a última liberada primeiro, então d[3] = 30; d[1] = 10; enumera 3,1. Cada chave mantém o seu tipo (uma chave int volta como número, uma long como BigInt, uma char como char) e é encontrada como o comparador padrão do tipo da chave a encontra: por valor para um record, uma struct, uma tupla, um decimal ou uma data, por identidade para um número, uma string, um char, um bool, um enum ou um Guid, e pela igualdade do próprio valor onde o tipo não decide (object, uma interface, um parâmetro de tipo, uma classe), então dois records iguais sob object são uma chave só; o elemento array de uma chave tupla pela referência (Desde 0.2.0-preview.61). new Dictionary<K, V>(other) copia, um foreach (var (k, v) in d) com desconstrução funciona em todo dicionário, os ordenados incluídos, TryAdd, ContainsValue, Remove(key, out value), Keys.Contains e Keys.Count respondem como os do .NET, e o ToDictionary constrói a mesma classe. Add, um inicializador de coleção e o construtor que copia pares recusam uma chave que já está lá com o ArgumentException do .NET, nas palavras de cada coleção, enquanto o indexador, o [chave] = valor de um inicializador de objeto e o TryAdd substituem ou recusam, e new KeyValuePair<K, V>(chave, valor) e KeyValuePair.Create constroem o par que um dicionário entrega (Desde 0.2.0-preview.60). Uma chave adicionada enquanto um foreach percorre o dicionário encerra o percurso com uma InvalidOperationException, como o enumerador do .NET faz, enquanto uma sobrescrita, uma remoção e o Clear seguem, e um dicionário ordenado recusa qualquer mudança. Um dicionário no estado renderizado no servidor, no resultado ou nos argumentos de uma Server Action, ou no payload de um tópico cruza como os seus pares [chave, valor] e chega como a classe, com as chaves no seu tipo, na ordem em que era enumerado, chaves inteiras incluídas: um objeto JSON as teria listado primeiro e em ordem crescente (Desde 0.2.0-preview.61, #437). Um comparador que pede o que o browser já faz (null, EqualityComparer<T>.Default, StringComparer.Ordinal) constrói o dicionário, e um ordenado mantém a ordem que ele pede (Desde 0.2.0-preview.61), enquanto um que muda a igualdade, como StringComparer.OrdinalIgnoreCase, falha o build com EQ2007. Desde 0.2.0-preview.60. As suas Keys e Values são vistas vivas, como as do .NET: uma lida depois de o dicionário mudar mostra a mudança, uma chave adicionada enquanto uma é percorrida encerra o percurso, o Contains das próprias chaves acha uma chave como o dicionário acha, e cada elemento volta como é, um delegate incluído. Uma consulta LINQ sobre uma vista, como sobre qualquer fonte, lê a fonte inteira antes do primeiro callback (#685). EnsureCapacity responde o primo em que o .NET se fixa, um construtor com capacidade e uma cópia dimensionam o dicionário como os do .NET, e TrimExcess compacta as entradas, de modo que a próxima chave vai depois das outras (Desde 0.2.0-preview.61).
•
✅ HashSet<T>, ISet<T> e IReadOnlySet<T> → o set do runtime ($eq.collections.hashSet), construído onde quer que um seja feito (um construtor, um inicializador, uma collection expression, o ToHashSet, um valor que veio do servidor). Ele guarda os elementos por posição na tabela em que o dicionário do runtime guarda as chaves: um elemento adicionado depois de uma remoção ocupa a posição do removido, a última liberada primeiro, então { 3, 1 } menos 3, mais 5, enumera 5,1, e cada operação de conjunto (UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, RemoveWhere), uma cópia e o TrimExcess deixam as posições onde as do .NET ficam. Um elemento é encontrado como o comparador padrão do seu tipo o encontra, a regra que as chaves de um dicionário seguem: uma data, um decimal, uma tupla e um record pelo valor, NaN igual a NaN. Add responde se o elemento era novo, os predicados de conjunto, CopyTo e EnsureCapacity respondem como os do .NET, e um elemento adicionado durante um foreach encerra o percurso com uma InvalidOperationException. O TryGetValue ainda não é tratado e falha o build (EQ1004); um comparador que muda a igualdade também falha (EQ2007) (Desde 0.2.0-preview.61).
•
✅ ContainsKey / TryGetValue nunca respondem por "constructor", "toString" ou qualquer outra coisa em Object.prototype. Desde 0.2.0-preview.15; GetValueOrDefault também, desde 0.2.0-preview.58.
•
✅ TryGetValue e GetValueOrDefault respondem a uma chave ausente com default(TValue) em todo dicionário, os ordenados incluídos: 0 para um int, false para um bool, null para uma referência, então d.TryGetValue(k, out var n); d[k] = n + 1; conta. O receptor, a chave e um padrão explícito são avaliados uma vez cada, na ordem em que foram escritos; um out _ não recebe nada, e um campo ou elemento como out é escrito onde o C# o escreve. A forma estática, CollectionExtensions.GetValueOrDefault(d, key), lê o dicionário que o primeiro argumento passa. Desde 0.2.0-preview.58.
•
✅ Queue<T>, Stack<T>, LinkedList<T>, SortedSet<T>, SortedDictionary<K,V>, SortedList<K,V> → coleções compatíveis do runtime, sob $eq.collections.*. Uma ordenada ordena como o Comparer<T>.Default ordena o tipo do elemento: um texto na cultura atual, um double com o NaN primeiro, um enum pelo valor, um decimal, uma data e um tipo seu que implementa IComparable pelo CompareTo, e o StringComparer.Ordinal por unidade de código. O Contains de uma fila e de uma pilha acha um decimal, uma data ou um record pelo valor (Desde 0.2.0-preview.60). Cada uma escreve o JSON que o .NET escreve, um array na ordem em que enumera, uma pilha a partir do topo, de modo que um argumento de Server Action chega ao servidor como a coleção (Desde 0.2.0-preview.61). O CopyTo de uma fila e de uma pilha, e o CopyTo, UnionWith, IntersectWith, ExceptWith, SymmetricExceptWith, IsSubsetOf, IsProperSubsetOf, IsSupersetOf, IsProperSupersetOf, Overlaps, SetEquals e RemoveWhere de um conjunto ordenado respondem como os do .NET, com as recusas nas palavras do .NET, e de dois elementos que um conjunto ordenado acha iguais (1.0m e 1.00m) uma união e uma interseção guardam o que o .NET guarda. Uma fonte nula entregue ao construtor de uma lista, uma fila, uma pilha, uma lista encadeada, um conjunto ordenado ou qualquer dicionário lança a ArgumentNullException que o .NET lança, nomeando collection ou dictionary, onde o navegador montava uma vazia (uma lista lançava um TypeError), uma capacidade negativa de fila ou de pilha é recusada, e um texto copiado para uma delas é lido pelos seus chars UTF-16 (Desde 0.2.0-preview.62, #723, #726). Um foreach sobre uma fila, uma pilha, uma lista encadeada ou um conjunto ordenado termina no passo seguinte a uma mudança da coleção, inclusive no que vem depois do último elemento, com a InvalidOperationException que o .NET lança, nas palavras do .NET: uma mudança é o que o .NET conta como uma, incluindo o Add de um elemento que o conjunto ordenado já tem e o Remove de um que ele não tem, e um valor escrito no nó de uma lista encadeada não é mudança. O Reverse() de um conjunto ordenado o percorre a partir do maior elemento e é recusado da mesma forma (Desde 0.2.0-preview.62, #765). O RemoveWhere de um conjunto ordenado pergunta ao predicado na ordem do conjunto, onde o .NET percorre a sua árvore em largura (#766).
System.Threading.Tasks
•
✅ Task, Task<T> → Promise.
•
✅ Task.Delay → wrapper de setTimeout.
•
✅ Task.WhenAll, Task.WhenAny.
•
⚠️ Task.Run executa na thread principal (microtask), NÃO numa thread de fundo.
Outros utilitários
•
✅ Console.WriteLine → console.log.
•
✅ Math.* (Min, Max, Abs, Round, etc.) → Math.*.
•
✅ Guid (NewGuid, Empty, Parse, TryParse, new Guid(texto)) → crypto.randomUUID() para um novo, e o seu texto canônico, o formato D em minúsculas, para um lido de texto em qualquer formato que o .NET lê, então duas grafias de um Guid são um valor só para ==, um dicionário e um conjunto (Desde 0.2.0-preview.60).
•
✅ Regex → RegExp do JavaScript.
🌐 Pacotes do ecossistema
eQuantic.UI.Lucide / Heroicons / RadixIcons / TablerIcons / Phosphor / SimpleIcons / BootstrapIcons / Iconoir / ...
Propósito: conjuntos de ícones abrangentes. Contêm:
•
Lógica de resolução de SVG
•
Componentes de ícone especializados
•
Um IconGlyph gerado por ícone, como propriedade para o trimmer poder descartar os que ninguém nomeia
Pacote
Propósito
Status
eQuantic.UI.Charts
Gráficos escritos uma vez, desenhados por todos os realizers.
✅
eQuantic.UI.Lottie
Animações de alta performance.
✅
eQuantic.UI.Image
Otimizada (Lazy/Blur/Priority).
✅
🧩 Padrões do framework
Padrão
Interface
Descrição
Status
Gestão de metadados
IHandleMetadata
Tags de SEO e elementos do head.
✅
Gestão de assets
IRequireAssets
Injeção dinâmica de script/estilo.
✅
Server Actions
[ServerAction]
RPC seguro do cliente para o servidor.
✅
Componentes compostos
N/A
Padrões de subcomponentes semânticos.
✅
Biblioteca de classes compartilhada
ProjectReference
Um tipo de um projeto referenciado que uma página usa é um módulo do bundle: uma classe estática, um record, um componente e as strings dele. Veja Fluxo de build (Desde 0.2.0-preview.62).
✅
⚠️ Limitações e ressalvas
1.
Código bloqueante:
•
❌ .Wait() e .Result em Tasks NÃO são suportados. Você tem que usar await. Bloquear a thread principal congela a interface do browser.
2.
Reflexão:
•
❌ System.Reflection é largamente sem suporte.
•
✅ typeof(T).Name e nameof(...) são constantes suportadas.
3.
Sistema de arquivos:
•
❌ System.IO (File, Directory) num componente de cliente é um erro de build (EQ2112): o browser não tem sistema de arquivos para oferecer.
•
Use Server Actions para lidar com operações de arquivo.
4.
Números:
•
✅ int/double/float mapeiam para números do JS. A divisão inteira trunca (Math.trunc), e uma divisão ou um resto por zero, ou MinValue por -1 em int e long, lança a exceção que o .NET lança (Desde 0.2.0-preview.58); um float é arredondado para precisão simples em todo lugar onde é produzido, exatamente como o RyuJIT arredonda, a atribuição composta e o incremento de um alvo float? incluídos (Desde 0.2.0-preview.58), e o Math.Round é o do .NET: meio exato, todo modo de MidpointRounding.
•
✅ decimal é exato de ponta a ponta: compilado para o tipo compatível Decimal do runtime (mantissa BigInt + escala), então 0.1m + 0.2m == 0.3m é true. Decimais também atravessam o fio como strings JSON (EqJson) e são hidratados de volta em Decimal no cliente (o padrão Decimal do campo guia uma coerção que preserva o tipo, veja hydrate-value.ts), então os decimais vindos do servidor mantêm todos os 28 dígitos em vez de arredondar por um double. Um decimal lido de texto (decimal.Parse/TryParse, Convert.ToDecimal("…")) segue a gramática do .NET sob o NumberStyles que a chamada indica e arredonda como o parser do .NET arredonda. É lido na cultura invariante, e a chamada diz isso com CultureInfo.InvariantCulture: sem cultura o build avisa (EQ2110) e com outra falha (EQ2108), porque o browser não lê o texto de nenhuma outra cultura. Um double ou um float convertido em decimal guarda os 15 ou 7 dígitos significativos do .NET: (decimal)(0.1 + 0.2) é 0.3. Desde 0.2.0-preview.58.
•
✅ Uma constante é o seu valor no seu tipo C#: decimal.MaxValue, MinValue, Zero, One e MinusOne são decimais exatos, escritos decimal ou Decimal, e uma constante long como TimeSpan.TicksPerSecond é um BigInt, então encontra qualquer outro long. Um literal decimal é o seu valor, então 1_000.5m funciona, e o padrão de um parâmetro preenchido para um argumento que um nomeado pula mantém o seu tipo: um decimal, um long, um char, uma string com aspas. Uma constante alcançada pelo nome nu sob um using static é escrita do mesmo jeito. Uma constante de um tipo enum é um valor desse enum, não o seu número, e uma constante decimal casa pelo valor num padrão, num braço de switch ou num case (d is 1m, case decimal.One:). Desde 0.2.0-preview.60.
•
✅ Todo outro tipo numérico também lê texto como o .NET lê: Parse/TryParse de int, uint, long, ulong, short, ushort, byte, sbyte, double e float, e Convert.ToInt32("…") e seus irmãos, leem o texto inteiro sob o NumberStyles do próprio tipo (Integer, ou Float | AllowThousands para um real) ou o que a chamada indica. Um texto que não é número lança FormatException ("12abc", "1e3" como int, "0x1F" sem HexNumber), um valor que o tipo não comporta lança OverflowException, um TryParse que falha deixa 0, um estilo hex ou binário lê os bits do tipo ("FFFFFFFF" é -1 num int), o texto de um long é um BigInt, e um float é arredondado uma única vez, a partir dos dígitos. A cultura é a do decimal: EQ2110 sem nenhuma, EQ2108 com qualquer uma que não seja CultureInfo.InvariantCulture. Desde 0.2.0-preview.58.
•
✅ O texto de um double e de um float é o do .NET: os dígitos mais curtos que voltam a ler o valor, notação fixa de 1e-4 até 1e16 para um double e 1e8 para um float, científica fora disso (1E+17, 1E-05), -0 para um zero negativo e Infinity para um infinito, o mesmo por ToString(), por ToString(CultureInfo.InvariantCulture), por uma concatenação e por uma interpolação; um float? ou double? nulo não escreve nada. Desde 0.2.0-preview.58.
•
✅ O texto de um número sem especificador é o da cultura, onde quer que o C# o escreva: uma concatenação, um buraco de interpolação simples ou alinhado, ToString(), Convert.ToString, string.Format("{0}"), string.Concat, string.Join, o Append e o Insert de um StringBuilder, e o texto de um record, como o .NET os escreve na cultura em vigor: -1,5 em pt-BR, um sinal de menos que a cultura soletra (o −5 do sv-SE), um float nos seus próprios dígitos, um decimal com a sua escala, e nunca -0 para um inteiro que o JavaScript guarda como zero negativo. Um valor escrito para uma máquina diz isso com ToString(CultureInfo.InvariantCulture), que mantém a forma em toda cultura. *Desde 0.2.0-preview.61*.
•
✅ Também pelo formatador: um float formatado com alinhamento ou especificador ($"{f,8}", $"{f:G}"), ToString("G"), ToString("R") e um float passado a string.Format escrevem os próprios dígitos, G e R na notação do .NET (1E+21). Um marcador de string.Format alinha ({0,5}, {0,-5}), um nulo num espaço alinhado é preenchido, e um array params passado inteiro, um string[] ou uma collection expression incluídos, são os seus valores. O provider do string.Format segue a política do ToString: CultureInfo.InvariantCulture formata de forma invariante, os padrões de data e o ¤ genérico da moeda incluídos, CultureInfo.CurrentCulture ou um nulo como uma chamada sem provider, e qualquer outro é EQ2108. O ToString() de um bool é True ou False. Um limite: um float encaixotado num object que o compilador não enxerga (object o = 0.1f; $"{o}", ou um object[] montado em tempo de execução) é o número simples por baixo. Desde 0.2.0-preview.58.
•
✅ Um número sai pelo seu especificador como o .NET o escreve: E e e escrevem os dígitos da mantissa, o sinal do expoente e pelo menos três dígitos de expoente ((12345.0).ToString("E2") é 1.23E+004). F, N, P, C, G e uma máscara própria arredondam o valor EXATO do número, o valor binário de um double incluído ((0.1).ToString("F20") é 0.10000000000000000555) e todos os dígitos de um long e de um decimal (9007199254740993L.ToString("N0") é 9,007,199,254,740,993), com uma metade exata arredondada para o par num double e num float e para longe do zero num decimal e num inteiro ((2.5).ToString("F0") é 2, (2.5m).ToString("F0") é 3, 125.ToString("E1") é 1.3E+002). N, F, C e P são dispostos a partir do NumberFormatInfo da própria cultura, que viaja com cada página: os seus padrões, símbolos, separadores, tamanhos de grupo e dígitos padrão ((1234.5).ToString("N") é 1,234.500 em en-US num servidor cujo .NET lê o ICU), em dígitos ASCII em toda cultura, a de ar-EG incluída, como o .NET os escreve (*Desde 0.2.0-preview.61*). Uma cultura trocada sem servidor a quem perguntar é disposta pelo Intl.NumberFormat v3 (ES2023): Chrome e Edge 106, Firefox 116 e Safari 15.4 o têm. X e B (binário) escrevem um inteiro negativo na largura do seu tipo (((short)-1).ToString("X") é FFFF, o de um int é FFFFFFFF). Uma máscara própria é desenhada como o .NET a desenha: seções (0.00;(0.00);'zero'), texto entre aspas e escapado, % e ‰, expoentes (0.00E+00) e as vírgulas antes do ponto que dividem por mil (#,##0,). A precisão pode passar de 100 dígitos, como o .NET permite. Os números são agrupados como o .NET agrupa (es-ES escreve 1.234) e levam o sinal de menos da cultura (o − do sv-SE), e um especificador que o tipo não aceita lança exceção, como no .NET ((2.5).ToString("D")). Desde 0.2.0-preview.59. Um double com D, X ou B lança exceção seja o que for que contenha, nint e nuint são inteiros de 64 bits, e o sinal de por mil (o ؉ do ar-SA), os sinais de um expoente e o sinal de porcentagem são os da própria cultura. Desde 0.2.0-preview.61.
•
✅ long/ulong agora são exatos: compilados para BigInt do JS pelo helper compatível long. 9007199254740993L + 1L é 9007199254740994 (um número JS comum arredondaria para …992). Literais viram BigInt (5L → 5n), e também um literal sem sufixo que só um long comporta (637000000000000000 → 637000000000000000n, Desde 0.2.0-preview.58); operandos de aritmética/comparação são embrulhados em long() (que coage number/string → bigint) para que expressões mistas nunca lancem. No fio, inteiros de 64 bits atravessam como strings JSON (Server Actions + estado do SSR, via EqJson) para que valores além de 2^53 sobrevivam à ida e volta. Outros sufixos de tipo numérico (1.5f, 100u) são removidos.
5.
Segurança entre threads:
•
Como o JS é single-threaded, instruções lock são compiladas para fora (ignoradas).
•
Thread.Sleep não é suportado (use Task.Delay).
🧪 Cobertura e conformidade da BCL .NET
A fidelidade da transpilação é imposta por um arnês de conformidade (tests/eQuantic.UI.Conformance.Tests): cada caso roda a mesma expressão C# de dois jeitos, transpilada para JS (executada pelo Bun embarcado) e avaliada diretamente em .NET (Roslyn scripting), e afirma resultados idênticos. A suíte conta seus próprios casos (mais de mil quando isto foi escrito, e crescendo a cada construção); todos estão verdes.
(Mais recente: todo dicionário pela classe de dicionário do runtime, $eq.collections.dictionary.) Isso cobre tanto expressões quanto blocos de instruções (controle de fluxo: if/for/foreach/while/switch/ try-catch-finally/funções locais; o bloco roda numa IIFE e o valor retornado é comparado).
Toda construção resolve por um de três mecanismos (veja docs/DOTNET-COVERAGE-PROGRAM.md):
1.
Estratégia nativa: JS idiomático quando o runtime tem um equivalente.
2.
Helper compatível com .NET no runtime: semântica .NET fiel onde o JS não tem nenhuma. O transpilador emite esses sob um único namespace $eq (organizado por domínio), trazido com **um import por módulo**, import { $eq } from "@equantic/runtime" (resolvido pelo import map da página), em vez de N imports soltos de helpers, e $eq.* nunca pode colidir com um identificador do usuário no escopo gerado:
•
$eq.num: dec (Decimal exato), decParse/decTryParse/decConvert (um decimal de texto ou de Convert), intParse/intTryParse/intConvert e realParse/realTryParse/realConvert (um inteiro, um double ou um float de texto ou de Convert), decFromDouble/decFromSingle (um número binário em decimal), long (Int64 via BigInt), double/single (o texto de um número)
•
$eq.math: round (arredondamento bancário)
•
$eq.convert: toBase64String, toBase64CharArray, toHexString, toHexStringLower (a codificação de um array, como o Convert do .NET a escreve)
•
$eq.text: format (formatação de número/string), stringBuilder
•
$eq.time: dateTime, timeSpan, dateOnly, timeOnly, dateTimeOffset
•
$eq.enums: text, parse, tryParse, zero, names, values, isDefined (um enum como o .NET o lê, a partir da forma que o eqc escreve em cada chamada)
•
$eq.collections: queue (FIFO), stack (LIFO), dictionary (guardado por posição como o do .NET, chaves encontradas por identidade ou por valor), linkedList, sortedSet/sortedDictionary/sortedList (ordenados por chave)
•
$eq.nullable: arith, cmp, unary (operadores Nullable<T> elevados: aritmética e operadores unários que propagam nulo, relacional falso em nulo)
•
$eq.equals: igualdade estrutural (por valor) para records/structs/tuplas (dá suporte a ==, .Equals, Contains, Distinct)
•
$eq.hash: of (GetHashCode(), que concorda com $eq.equals, então valores que Equals acha iguais têm o mesmo hash, um tipo de valor do vocabulário como Point ou TypeStyle incluído, e um tipo que o sobrescreve responde o seu), instance (a mesma chamada numa referência, recusada quando ela é nula, como o .NET lança NullReferenceException, enquanto um Nullable<T> vazio responde 0), group (o method group value.GetHashCode, que lê o receptor uma vez e recusa um nulo onde o delegate é criado), combine (HashCode.Combine, pelos parâmetros mesmo quando nomeados) e identity (o object.GetHashCode de uma classe que não o sobrescreve, e o de um array, mudem os itens como mudarem). Os números do próprio .NET não são estáveis entre processos, então o browser mantém o contrato, nunca o número do servidor (Desde 0.2.0-preview.60)
•
$eq.css: styleBuilder, classBuilder, joinClasses, whenClass (o subsistema de estilo)
3.
Falhar no que não é suportado: uma construção sem representação possível em JS agora levanta um erro de build (com um código EQ estável) em vez de ser emitida verbatim em silêncio. Duas camadas:
•
UnsupportedConstructStrategy (EQ2001): intrínsecos de referência tipada (__makeref, __refvalue, __reftype), tipos ponteiro, ponteiros de função.
•
goto/goto case/goto default (EQ2002): sem equivalente em JS; reestruture com laços/condicionais. (Blocos unsafe/fixed/lock desembrulham para o corpo deles, sendo o lock um no-op single-threaded, e um rótulo solto cai para a instrução interna dele.)
•
Fronteira cliente/servidor do SemanticValidator (EQ21xx): chamadas a System.IO, System.Net.Http, System.Net.Sockets, EF Core / System.Data, threading do SO (Thread/Monitor/Mutex), Process, InteropServices (P/Invoke), Reflection.Emit a partir de um componente cliente. (System.Threading.Tasks não é proibido: async mapeia para Promise.) A correção é mover a chamada para um [ServerAction].
Qualquer outra construção que não bata em nenhuma estratégia é erro (EQ1001 para expressão, EQ1002 para statement, EQ1003 para o resto) e nada é emitido por ela. Antes passava verbatim com um aviso, o que dava uma build que sucedia e uma página que quebrava — recusar o que não consegue representar é justamente o ponto. Os diagnósticos saem na forma canônica do MSBuild, então falham o dotnet build e a IDE linka. A lista completa está em Diagnósticos.
Área
Status
Notas
Aritmética / bitwise / comparação
✅
a divisão inteira trunca; %, deslocamentos, & | ^ ~ nativos para um int; os & | ^ >> ~ de um uint continuam sem sinal, e os deslocamentos de um long mascaram a contagem em seis bits e descartam os bits além de 64, como os do .NET (Desde 0.2.0-preview.58); checked(expr) lança OverflowException, unchecked(expr) dá a volta em 32 bits (\| 0/>>> 0), uma negação incluída, e um await dentro de qualquer um dos dois é do próprio método (Desde 0.2.0-preview.60). Estouro de int no contexto padrão não dá a volta (float64 do JS).
Math.* / MathF.*
✅
a mesma tabela de double.* / float.*, com as composições do próprio .NET; o Round é o do .NET (meio exato, todo modo)
decimal
✅
exato via Decimal (literais + + - * / % == != < > <= >=, o resto com o sinal do dividendo, Desde 0.2.0-preview.58 para %); as constantes MaxValue, MinValue, Zero, One e MinusOne (Desde 0.2.0-preview.60); decimal.Parse/TryParse pela gramática e pelo arredondamento do .NET, o NumberStyles que a chamada indica incluído; Convert.ToDecimal e os casts de double/float com os 15 e 7 dígitos do .NET
Constantes numéricas
✅
int.MaxValue, double.Epsilon, … → literais
Parsing / Convert.*
✅
Parse/TryParse de todo tipo numérico, e Convert.ToInt32/ToInt64/ToDouble/… de texto, pela gramática do .NET (Desde 0.2.0-preview.58); bool.Parse/TryParse e Convert.ToBoolean de texto como o Boolean.TryParse do .NET lê: "True" ou "False" em qualquer caixa, sem espaço em branco nem NULs nas pontas, e as exceções do .NET, e Convert.ToBoolean de um número como o fato de ele não ser zero, de um bool como ele mesmo, e de um char ou de uma data como a InvalidCastException do .NET (Desde 0.2.0-preview.59); Convert.ToString/…; Convert.ToDateTime de um texto como o DateTime.Parse o lê, na cultura que a chamada nomeia, e de um texto nulo como MinValue (Desde 0.2.0-preview.62, quando respondia o texto); Convert.ToBase64String, ToBase64CharArray, ToHexString e ToHexStringLower escrevem a codificação de um byte[] que o .NET escreve, com o seu deslocamento, o seu comprimento e as quebras de linha a cada 76 caracteres, e recusam o que o .NET recusa, nas palavras dele; um span que não é uma expressão de coleção falha o build (EQ1004) (Desde 0.2.0-preview.62, quando cada um escrevia o 1,2,255 do array)
Strings
✅
Substring/IndexOf/Replace/Split/Pad/Trim/Trim(char)/Concat/Format/Join/IsNullOrEmpty/IsNullOrWhiteSpace; Trim(chars) e as suas duas metades leem a string e depois os caracteres uma vez, um await entre eles incluído, e nenhum caractere (um array vazio ou nulo) apara o espaço em branco, como o .NET (Desde 0.2.0-preview.60); Trim(), suas duas metades, IsNullOrWhiteSpace e um Split() sem separador leem o espaço em branco do .NET, onde o do JavaScript deixa o U+0085 e leva o U+FEFF (Desde 0.2.0-preview.58); os métodos da própria string Equals, StartsWith, EndsWith, Contains, IndexOf e LastIndexOf (com início e contagem) e Replace com um StringComparison respondem por essa comparação, escrita por extenso ou guardada numa variável: OrdinalIgnoreCase mantém a caixa do .NET (o sinal de Kelvin, o s longo e o i sem ponto não são k, s e I, e um par de surrogates é lido como o seu code point), um início ou uma contagem fora da string lança a exceção do .NET, Replace escreve a substituição como texto, e CompareTo ordena pela cultura atual, como string.Compare (Desde 0.2.0-preview.60). IndexOf e LastIndexOf de uma string a partir de um início, e por uma contagem, sem comparação param na contagem, leem o início do LastIndexOf como o último char que uma ocorrência pode cobrir, e lançam a exceção do .NET para um intervalo fora da string ou um valor nulo (Desde 0.2.0-preview.62, onde a contagem era ignorada e LastIndexOf("bc", 4) respondia a ocorrência que começa em 4). IndexOf(char, startIndex[, count]) e LastIndexOf(char, startIndex[, count]) buscam nesse intervalo e lançam a exceção do .NET para um início ou uma contagem fora da string: o início do LastIndexOf fica sobre um char da string, e uma string vazia responde -1 (Desde 0.2.0-preview.61, onde o início era ajustado e a contagem ignorada). Uma BUSCA por uma comparação de cultura, ou com um CultureInfo, falha o build (EQ1004), porque o navegador não tem busca sensível à cultura: busque por Ordinal ou OrdinalIgnoreCase, ou compare strings inteiras com Equals ou string.Compare. As sobrecargas sem comparação buscam por ordinal no navegador e pela cultura atual no .NET (#532).
char.*
✅
ToUpper/ToLower/IsDigit/IsLetter/IsWhiteSpace/… (cientes de Unicode; IsWhiteSpace é o conjunto do .NET, com o U+0085 e sem o U+FEFF, Desde 0.2.0-preview.58; ToUpper, ToLower e o seu par invariante mudam a caixa de um char como a de uma string de um, na cultura que a chamada nomeia, e uma cultura nula lança a ArgumentNullException do .NET, Desde 0.2.0-preview.62)
StringBuilder
✅
tipo compatível: Append(incl. bool→"True"/"False")/AppendLine("\n")/Insert/Remove/Replace/Clear/Length/ToString. As sobrecargas que contam ou recebem um intervalo escrevem o que o .NET escreve, Append('x', 3), Append(texto, início, quantidade), Insert(i, texto, quantidade), Replace num intervalo e ToString(início, comprimento), um char[] como os seus caracteres e um nulo como nada, e cada recusa é a do .NET (Desde 0.2.0-preview.61). AppendFormat e AppendJoin acrescentam o que string.Format e string.Join escrevem, Capacity segue os chunks que o .NET aloca a cada edição, MaxCapacity e EnsureCapacity respondem, o indexador Chars lê e escreve um char, Length corta o texto ou o completa com \0, Equals(StringBuilder) compara o texto e CopyTo o copia, cada um recusando o que o .NET recusa; um Append($"…") ou AppendLine($"…") interpolado acrescenta cada parte por sua vez, como o handler do .NET, de modo que um buraco que lê o builder vê as partes anteriores, um argumento nomeado vai para o seu parâmetro, e um texto maior do que a string do navegador comporta é a OutOfMemoryException do .NET; um provedor que não seja a cultura atual em Append, GetChunks e as sobrecargas de span falham o build (Desde 0.2.0-preview.61)
CancellationTokenSource / CancellationToken
✅
os do runtime, como os do .NET: uma fonte (com um atraso ou sem), o seu Token, IsCancellationRequested, Cancel, CancelAfter, Dispose e CreateLinkedTokenSource; o None de um token (e default, e new CancellationToken(bool)), IsCancellationRequested, CanBeCanceled, ThrowIfCancellationRequested (uma OperationCanceledException, "The operation was canceled.") e o Register de um callback, cujo registro faz Dispose e Unregister. Os callbacks rodam uma vez, o último registrado primeiro, um registrado tarde roda na hora e recebe de volta o registro padrão (o seu token é None), e os que lançam são reunidos numa AggregateException, como no .NET. Um método dos três passado como callback (token.Register(source.Cancel)) mantém o seu receptor, e uma fonte vinculada, descartada, solta os tokens que segue. Uma fonte descartada solta também os seus callbacks, um registro ainda se desfazendo uma vez depois disso, e new CancellationTokenSource(delay, timeProvider) falha o build com EQ2004: a fonte do runtime é medida pelo relógio real. Um atraso é aceito até 4294967294 ms, como o .NET o aceita, e um mês é esperado até o fim, embora o timer de um browser segure cerca de 24,8 dias. default(CancellationTokenRegistration) é o registro de nada, cujo token é None. Qualquer outro membro dos três falha o build com EQ2004 (Desde 0.2.0-preview.61)
LINQ
✅
Where/Select/SelectMany/Where-Select(indexado)/OrderBy/Distinct(By)/GroupBy/ToDictionary/ToLookup/Zip/Chunk/MinBy/MaxBy/Take(While)/Skip(While)/Aggregate/Sum/Min/Max/Average/Count/Any/All/First/Last/Concat/Reverse/Join/GroupJoin/ThenBy/ThenByDescending (Join/GroupJoin = hash join que preserva a ordem sobre chaves primitivas; OrderBy+ThenBy = uma única ordenação composta estável, com a fonte copiada). O IGrouping vindo de GroupBy/ToLookup é usável como sequência (iterar, g.Select/g.Sum/g.Count()) e expõe g.Key; o indexador [key] de ILookup não é modelado. Um comparador que pede o padrão passa por ToDictionary, ToLookup, GroupBy, Distinct e ToHashSet, e qualquer outro faz a build falhar (EQ2007) (Desde 0.2.0-preview.61).
Coleções
✅
List, Dictionary, HashSet (incl. inicializadores e .Count; um set é o set do runtime, guardado por posição e achando os elementos pelo valor onde o tipo do elemento os acha assim, Desde 0.2.0-preview.61); compatíveis de Queue/Stack/LinkedList; família ordenada SortedSet/SortedDictionary/SortedList (enumeração ordenada por chave). Todo dicionário é a classe de dicionário do runtime, $eq.collections.dictionary, enumerando por posição como o do .NET, com uma chave de record/struct/tupla encontrada por valor (construção e cópia, get/set em d[k], ContainsKey/Add/Remove/Clear/TryGetValue/GetValueOrDefault/TryAdd/ContainsValue, Keys/Values/Count, foreach) (Desde 0.2.0-preview.60). Ler uma chave ausente lança KeyNotFoundException, assim como uma atribuição composta e um incremento sobre ela (Desde 0.2.0-preview.58 para o incremento). O indexador ILookup[key] devolve o grupo (ou vazio para uma chave ausente). O Add e o Clear de ICollection<T> alcançam a coleção por trás da interface, e IList<T> e IReadOnlyList<T> leem e escrevem um tipo seu pelo seu indexador e o contam pelo seu Count (Desde 0.2.0-preview.61).
enum
✅
string com o nome do membro, um enum [Flags] o seu número (igualdade/switch/ternário, texto e formatos, os estáticos de Enum)
long/ulong
✅
exato via BigInt (helper long); literais 5L→5n, e as constantes também (TimeSpan.TicksPerSecond → 10000000n, Desde 0.2.0-preview.60), no fio como string JSON
DateTime
✅
tipo compatível DateTime com precisão de tick: construtores, componentes, Add* até o tick, como no .NET 7+ (a fração truncada, uma contagem ou um resultado fora do intervalo recusados com as palavras do .NET, AddMicroseconds incluído, Desde 0.2.0-preview.59), -→TimeSpan, comparações, .ToString()/format; fio ISO-8601 + hidratação. ToString(), ToString(format) e ToString(format, provider) saem como o .NET os escreve (sem especificador, ou com um nulo ou vazio, é o G da cultura atual, e nenhum fuso horário move as partes do valor, fora o U): todo especificador padrão de uma letra (de d a Y, s, u e R pelas partes do próprio valor, o por elas com o fuso que o seu tipo nomeia, U levado para UTC quando ainda não é UTC), uma letra que não é especificador padrão recusada como o .NET a recusa, uma máscara própria token a token com a sua fração de segundo (HH:mm:ss.fff), os separadores da cultura para / e : e a era para g (Desde 0.2.0-preview.61), os padrões da cultura invariante para CultureInfo.InvariantCulture ou sem cultura em vigor, e uma cultura nomeada recusada no build (EQ2108); ToShortDateString() e os irmãos são d, D, t e T (Desde 0.2.0-preview.60). Add, Subtract, AddMonths, AddYears e os operadores + e -, as formas compostas incluídas, recusam um resultado fora do calendário com as palavras do .NET, os operadores nomeando o seu parâmetro t (Desde 0.2.0-preview.60). Todo construtor que o compilador deixa passar constrói o valor que o .NET constrói, cada argumento no lugar do seu parâmetro, um nomeado incluído: os componentes com um milissegundo, um microssegundo e um tipo (Kind), os ticks com um tipo, e um DateOnly com um TimeOnly, um componente, um tipo ou ticks fora do intervalo recusados com as palavras do .NET, e uma sobrecarga que recebe um Calendar recusada no build (EQ1004). Um valor carrega o seu Kind (Now e Today locais, UtcNow UTC, SpecifyKind), que a aritmética mantém e que a igualdade, a ordenação e o hash deixam de fora; ToLocalTime() e ToUniversalTime() convertem por ele no fuso horário do browser, Microsecond e Nanosecond respondem, e o JSON escreve um valor UTC com Z e um local com o seu deslocamento e os lê de volta como o System.Text.Json (Z uma hora UTC, um deslocamento levado para a hora local do browser), enquanto DateTime.Parse leva um fuso escrito para a hora local. Uma hora local feita de um instante guarda qual de duas horas repetidas ela é, como o .NET guarda ao lado do tipo, pela aritmética até o SpecifyKind, e o caminho de volta para UTC é aquele instante, e o texto de uma data escreve o seu tipo: o e K terminam uma hora UTC com Z e uma local com o seu deslocamento, e z, zz e zzz escrevem o deslocamento do fuso para o que não é UTC (Desde 0.2.0-preview.61)
TimeSpan
✅
tipo compatível TimeSpan com precisão de tick: From*, construtores, componentes/totais, + -, comparações, .ToString() "c" do .NET, e os seus formatos padrão e personalizados (Desde 0.2.0-preview.62); Parse e TryParse leem como o .NET lê sem formato, cada parte contra o seu limite (Desde 0.2.0-preview.62); fio "c" + hidratação
DateOnly / TimeOnly
✅
tipos compatíveis (.NET 6+): construtores, componentes, Add*(+ volta do TimeOnly, um só produto convertido como o .NET 9+ converte um double, Desde 0.2.0-preview.59, um DateOnly que sai do calendário recusado com as palavras do .NET, Desde 0.2.0-preview.60), comparações, ToString(), ToString(format) e ToString(format, provider) como o .NET os imprime na cultura em vigor: d e t sem formato, os especificadores padrão que cada tipo aceita e uma máscara própria das suas partes, qualquer outro a FormatException do .NET, e ToShortDateString(), ToLongDateString(), ToShortTimeString() e ToLongTimeString() como d, D, t e T (Desde 0.2.0-preview.61); fio ISO + hidratação
DateTimeOffset
✅
tipo compatível com precisão de tick (hora de parede + deslocamento, comparado pelo instante): construtores, componentes, MinValue/MaxValue (Desde 0.2.0-preview.58), Offset/UtcDateTime/LocalDateTime, ToOffset, Add* até o tick com AddMilliseconds/AddMicroseconds, a hora local e depois a hora UTC recusadas com as palavras do .NET (Desde 0.2.0-preview.59), AddMonths e AddYears incluídos (Desde 0.2.0-preview.60), From/ToUnixTime* (ToUnixTimeSeconds e ToUnixTimeMilliseconds um long, arredondado para baixo antes de 1970 como o .NET arredonda, Desde 0.2.0-preview.61), - →TimeSpan, comparações por instante, ToString() e as suas sobrecargas como o .NET os imprime na cultura em vigor: a data curta, a hora longa e o deslocamento sem formato, todo especificador padrão menos o U, com o e K escrevendo o deslocamento e u e R convertendo por ele (Desde 0.2.0-preview.61); fio ISO+offset + hidratação. Todo construtor constrói o valor que o .NET constrói: os componentes com um milissegundo e um microssegundo, os ticks, um DateOnly com um TimeOnly, e um DateTime, que sem deslocamento toma o do browser para uma hora que não é UTC; um deslocamento que não é de minutos inteiros, passa de catorze horas, ou não é zero para um DateTime UTC é recusado com as palavras do .NET. Now é o instante no deslocamento do browser, LocalDateTime e ToLocalTime() leem o fuso horário do browser, e Microsecond e Nanosecond respondem (Desde 0.2.0-preview.61)
record / struct / tupla de valor
✅
Semântica de valor. Records/structs são instâncias das suas classes twin (o posicional new Point(1,2) chama o construtor que o C# chama, e um inicializador de objeto é aplicado depois dele), tuplas são arrays com acesso a elemento por posição (t.Item1) e por nome declarado ((int X, int Y).X) → índice. ==/!=, .Equals, Distinct comparam estruturalmente via $eq.equals, e Contains, IndexOf e um set pelo comparador padrão do tipo do elemento, o membro array de uma tupla pela referência (Desde 0.2.0-preview.61); o with copia e substitui. Uma struct mutável e uma tupla de valor são copiadas onde o C# as copia, então uma escrita por um nome nunca aparece por outro a que o valor foi atribuído, passado, devolvido ou encaixotado: o twin copia na escrita, e copia o this onde ele sai do membro da sua struct (Desde 0.2.0-preview.61). A desconstrução var (a, b) = … funciona para tuplas (destructuring de array, com buracos de descarte) e records (destructuring de objeto pela ordem do Deconstruct). Records são emitidos como classes JS nomeadas, carregando os métodos de instância do usuário + um equals estrutural, um with que preserva o protótipo, e o toString do .NET; a semântica de valor não muda. O equals de um record compara o seu tipo em runtime (um Dog nunca é igual a um Animal), o que a sua base compara e todo campo que ele declara, inclusive um privado, e o seu texto nomeia os membros que o PrintMembers do .NET escreve, na ordem dele: os membros da base primeiro, depois os seus, inclusive uma propriedade calculada e uma propriedade cujo getter tem acessibilidade própria, deixando um override para a base, cada um uma vez, e Name { } quando não há nenhum. Dois membros de um record ou de uma struct que caem num mesmo nome do twin (struct S(int x) { public int X … }, um campo celsius ao lado de Celsius) fazem a build falhar (EQ1007); o parâmetro de um construtor primário lido só por um inicializador (struct Point(int x) { public int X { get; } = x; }) é do construtor, e compila; uma propriedade abstrata não guarda nada. Cada valor é escrito como o .NET o escreve: um bool como True, um enum pelo nome do membro, um null como nada e um número na cultura em vigor, e uma classe ou struct simples que não escreve texto próprio escreve o nome completo do seu tipo (My.App.Outer+In), onde o browser escrevia [object Object] ou o texto de um record; uma genérica ainda não (Desde 0.2.0-preview.61). Um parâmetro posicional cuja propriedade o record declara (record Box(int X) { public int X { get; set; } = X; }) é um membro só, o declarado (Desde 0.2.0-preview.61). Um membro começa como a sua declaração diz, seja qual for a expressão: um parâmetro posicional, o inicializador de uma propriedade ou de um campo (= 1.5m, = 5 num long, = new(), = "#" + Id), escrito uma única vez, no construtor da classe (Desde 0.2.0-preview.59), que roda todo inicializador em toda construção, seja o que for que um inicializador de objeto defina depois (desde 0.2.0-preview.61; veja Construtores acima). Os membros de um record e de um struct são convertidos como os de uma classe: um método async, um iterador, um parâmetro out ou ref e a variável que o padrão de um corpo de expressão declara (o is P p && p.X == X) funcionam nos seus métodos, operadores, conversões e propriedades calculadas, os próprios e os padrões que uma interface fornece, e um getter que faz yield devolve a sua sequência também numa classe (Desde 0.2.0-preview.60). O pipeline de build descobre records por varredura e emite cada um como módulo próprio; componentes que referenciam um record o importam automaticamente (reativo, sem lista fixa). Os valores do SSR são re-hidratados de volta na classe do record no cliente (recursivamente, restaurando records aninhados e membros compatíveis), então métodos/instanceof sobrevivem. Cobre records posicionais e de corpo e structs simples, mais herança de record (extends + super) e records genéricos, cuja igualdade compara o tipo fechado com que um valor foi construído: um Box<int> nunca é igual a um Box<double>, e um construído dentro de código genérico, cujos argumentos de tipo o build não conhece, não é tomado por outro tipo (Desde 0.2.0-preview.61). Dicionários com chave de record (Dictionary<RecordKey, V>) encontram uma chave por valor, para que chaves iguais por valor colidam como no .NET. Cada membro compara como o EqualityComparer<T>.Default compara o seu tipo, então um array, uma lista ou a interface de uma coleção (IReadOnlyList<int>) por referência, e o Equals de uma tupla compara os seus elementos do mesmo jeito, uma tupla de outra aridade diferente e um par nulo igual só a outro; uma tupla vista só como object, ou como uma interface que uma tupla implementa, é um array na web e compara pelos elementos (Desde 0.2.0-preview.61).
Nullable<T> (T?)
✅
HasValue/Value, GetValueOrDefault() (padrão ciente do tipo: 0/false/$eq.num.dec(0)/membro zero do enum/…) e GetValueOrDefault(fallback), ??; operadores elevados via $eq.nullable.*: a aritmética propaga null, o relacional (< > <= >=) é falso quando qualquer lado é nulo (não uma coerção numérica); uma atribuição composta (+=, *=, …), um incremento e um -, ~ ou + unário sobre um número nullable também são elevados, um nulo continuando nulo e um valor seguindo a regra do seu tipo (um float? arredonda, um byte? dá a volta, um decimal? continua exato; Desde 0.2.0-preview.58). O GetValueOrDefault() sem argumento em DateTime?/Guid?/struct devolve null, então use a forma com fallback ali.
Guid
✅
Guid.NewGuid()→crypto.randomUUID(), Guid.Empty, Guid.Parse/TryParse/new Guid(texto) → o texto canônico D em minúsculas (Desde 0.2.0-preview.60); fio como string