3 pontos por GN⁺ 2024-07-26 | 1 comentários | Compartilhar no WhatsApp
  • O PR #53725 do Node.js foi mesclado, adicionando suporte experimental para executar arquivos TypeScript com a flag --experimental-strip-types
  • Esse recurso remove a sintaxe de tipos do código-fonte TypeScript para convertê-lo em código-fonte JavaScript, sem realizar verificação de tipos durante a conversão
  • A implementação inicial não converte recursos exclusivos do TypeScript, como Enum e namespaces, portanto fica limitada a código que se torna JavaScript apenas com a remoção de tipos
  • A implementação escolheu @swc/wasm-typescript, pelo motivo de simplicidade, já que não exige adicionar Rust ou Go ao toolchain do Node.js
  • Na discussão, as principais limitações abordadas foram a regra de extensões de arquivo obrigatórias do ESM, a prática do TypeScript em que imports .js apontam para arquivos .ts reais, e o custo de sourcemaps e posições em stacks de erro

Como funciona o --experimental-strip-types

  • Ao definir --experimental-strip-types, o Node.js consegue executar arquivos TypeScript
  • O Node.js transpila o código-fonte TypeScript para código-fonte JavaScript
  • Durante a transpilação, a verificação de tipos não é realizada, e as informações de tipo são descartadas
  • Um exemplo é o seguinte
const foo: string = "foo";
const foo = "foo";

Por que escolher apenas a remoção de tipos e limitações iniciais

  • O ponto central deste PR é remover a sintaxe de tipos da sintaxe TypeScript
  • Outros runtimes também convertem para JavaScript recursos exclusivos do TypeScript que não existem em JavaScript, como enum
  • A implementação inicial deste PR não realiza esse tipo de conversão
    • Não é possível usar Enum
    • Não é possível usar namespaces
    • Outros recursos exclusivos do TypeScript também não são alvo de conversão
  • Há uma orientação para consultar typescript.md nas alterações do PR para detalhes de implementação e limitações

Escolha de @swc/wasm-typescript como ferramenta de implementação

  • @swc/wasm-typescript é um pacote pequeno, composto por um arquivo wasm e um arquivo JavaScript que faz seu binding
  • Outras ferramentas tinham o requisito de adicionar Rust ou Go ao toolchain do Node.js
  • O SWC também é usado no Deno para o mesmo propósito, e o autor do PR o vê como uma opção validada
  • No futuro, esse recurso poderia ser implementado na camada nativa
  • Este PR faz referência a nodejs/loaders#217

Discussão sobre resolução de imports e extensões

  • Imports sem extensão, como import foo from "./foo", não são compatíveis
  • O ESM do Node.js tem a regra de extensões de arquivo obrigatórias, então a falta de suporte a imports sem extensão é tratada como comportamento intencional
  • Foi levantado que, em codebases de módulos TypeScript, é comum usar a extensão .js na instrução de import mesmo quando o arquivo real é .ts
    • Exemplo: import foo from './foo.js'
    • Caso em que não existe ./foo.js no sistema de arquivos, apenas ./foo.ts
  • O autor do PR vê que esse problema pode ser tratado em iterações futuras, e que inferir extensões exigiria verificações adicionais no sistema de arquivos e heurísticas
    • Se ./foo.js não existir, verificar ./foo.ts
    • Se ./foo.ts não existir, verificar ./foo.js
  • Essa discussão é acompanhada em nodejs/loaders#208

Posições de origem, sourcemaps e stacks de erro

  • Na revisão, foi levantada a opinião de que posições de origem precisas em stacks de erro são necessárias no curto prazo
  • Uma possível abordagem é não alterar as posições de origem durante a remoção de tipos, evitando a geração e o mapeamento de sourcemaps
  • Foi mencionada a limitação de que sourcemaps têm custo em termos de uso de memória e desempenho, além de não serem simples
  • O autor do PR vê que, como o SWC já oferece suporte a sourcemaps, seria possível adicionar uma flag para habilitá-los
  • Também seria possível pedir ao SWC uma opção para substituir a sintaxe removida por espaços, de modo a não alterar a stack de erro original

Questões de design restantes antes da estabilização

  • O suporte a TypeScript se divide em dois eixos
    • Remoção da sintaxe de tipos
    • Ajuste das regras de resolução para aceitar arquivos .ts
  • Foi levantada a preocupação de que mudanças nas regras de resolução possam conflitar com loaders TypeScript existentes
  • O autor do PR respondeu que este PR não contém override das regras de resolução e que a extensão do arquivo é necessária
  • Foi sugerido que, por meio de allowImportingTsExtensions, é possível oferecer suporte ao formato import './file.ts', e que essa abordagem também é compatível com o Deno
  • Para executar diretamente o mesmo arquivo-fonte e também gerar saída JavaScript com uma ferramenta de build, considera-se que a responsabilidade de trocar file.ts por file.js pode ficar com a ferramenta de build

Mesclagem e trabalhos posteriores

  • O PR foi mesclado em nodejs:main em 24 de julho de 2024
  • O tamanho da alteração é de 8 commits, 89 arquivos, 2.190 linhas adicionadas e 25 linhas removidas
  • Foram aplicados rótulos como semver-minor, experimental, module, esm e notable-change
  • Houve discussão sobre mencionar esse recurso na página de orientação sobre TypeScript do site, e um PR relacionado de atualização também foi vinculado
  • Também foi discutida em uma issue posterior a possibilidade de incluir nos metadados de distribuição a versão do TypeScript usada pelo binário do Node para remoção de tipos

1 comentários

 
GN⁺ 2024-07-26
Opiniões do Hacker News
  • A remoção de tipos do TypeScript é impossível sem a sintaxe do TypeScript. A remoção de tipos não é uma operação em nível de tokens, e a sintaxe do TypeScript continua mudando.
    Por exemplo, foo < bar & baz > ( x ) no TypeScript 1.5 era parseado como (foo (x)), porque bar&baz não era uma expressão de tipo válida, mas depois que o operador de tipos de interseção foi adicionado, passou a ser parseado como foo<(bar & baz)>(x) e desambiguado para foo(x).
    Para continuar usando recursos recentes do TypeScript, será preciso compilar para JS ou manter a versão do Node sempre atualizada, e isso pode ser um compromisso difícil de aceitar para equipes que preferem ficar no Node LTS.

    • A equipe do Node também parece já ter considerado esse problema. Como há o precedente de o Node incluir o npm no pacote, mas permitir atualizá-lo separadamente, há discussões sobre fazer algo parecido com o transformador de TypeScript.
      A ideia seria manter uma versão estável no core, mas permitir uma atualização separada via NPM quando o TypeScript lançar novos recursos ou mudanças incompatíveis, ou quando o usuário quiser usar novos recursos experimentais.
      Assim, seria possível dar suporte a uma versão específica do TypeScript durante o ciclo de vida de 3 anos de uma versão do Node.js sem prender os usuários a ela: https://github.com/nodejs/loaders/issues/217
    • Do ponto de vista da remoção de tipos, houve muito mais versões do TypeScript em que a sintaxe ficou estável do que períodos em que ela foi instável. O exemplo voltou até a 1.5 justamente porque, desde a 2.x, ela tem sido bastante estável.
      A última grande mudança de sintaxe provavelmente foi os tipos condicionais da 2.8, quando uma sintaxe parecida com o operador ternário if entrou em posições de tipo. Para fazer verificação de tipos, o modelo de tipos mudou muito desde a 2.x, mas a sintaxe em geral ficou estável, e a maior parte da inovação do TypeScript também esteve no modelo de tipos e na inferência de tipos.
      Ainda está em estágio inicial, Stage 1, mas também existe uma tentativa de padronizar uma parte considerável da sintaxe de tipos do TypeScript para fins de remoção de tipos, não de verificação de tipos: https://github.com/tc39/proposal-type-annotations
    • O compilador também precisa ser atualizado junto. No fim, dá para descrever isso como “não é possível atualizar a versão do TypeScript independentemente da versão do Node.js”, mas provavelmente não será um grande problema.
    • Usar sinais de desigualdade como colchetes angulares realmente parece ter sido um erro.
    • Equipes que acompanham sempre o TypeScript mais recente e usam novos recursos da linguagem provavelmente também acompanharão a versão mais recente do Node. Pela minha experiência, upgrades do TypeScript exigiram migrações ou correções por causa de novos erros com mais frequência do que upgrades do Node.
      Equipes que não têm muito interesse no V8 ou em recursos recentes do Node e ficam no LTS provavelmente também terão menos interesse nos recursos mais recentes do TypeScript.
  • Se o Node.js puder executar arquivos TypeScript diretamente, o compilador TypeScript poderá ser usado apenas como verificador de tipos, em vez de remover tipos e transformar JavaScript. É parecido com o modo como, em Python, o verificador de tipos verifica os tipos e os deixa como estão, enquanto o interpretador ignora as dicas de tipo.
    Curiosamente, em Python surgiram vários verificadores de tipos populares que usam a mesma sintaxe de dicas de tipo, mas têm semânticas diferentes. Já em JavaScript, o TypeScript parece ter se tornado praticamente o único verificador de tipos popular.
    Também já ouvi falar de casos em Python em que as pessoas não verificam os tipos, apenas escrevem tipos no código-fonte como uma sintaxe de comentários mais conveniente. Se o Node.js der suporte a ignorar tipos, esse estilo também se torna possível em JavaScript.

    • O Flow, do Facebook, foi bastante importante no lado JavaScript alguns anos atrás, mas agora parece que o TypeScript venceu com bastante clareza.
    • Mesmo sem fazer verificação de tipos, a IDE pode usar dicas de tipo para melhorar o autocompletar e coisas do tipo, então adicionar tipos em alguns lugares ainda faz sentido.
    • Mesmo hoje, é possível adicionar tipos com JSDoc e validá-los com TypeScript sem compilação. O build fica mais rápido, e você obtém código que funciona em qualquer lugar sem processamento mágico nem etapas de remoção além de comentários.
      Pessoalmente, o maior incômodo de usar JSDoc era a sintaxe de importação, mas isso mudou a partir do TypeScript 5.5 e agora deixou de ser um problema.
    • Node.js não é o único runtime JavaScript. Até que todos os navegadores consigam executar TypeScript diretamente, ainda será preciso compilar TS para JS para o navegador.
      Ainda assim, alguns bundlers já fazem isso usando compiladores não oficiais, como o SWC, e o Node também está testando o SWC nesse recurso.
      Dicas de tipo não são apenas simples comentários; como o nome sugere, também são dicas para a IDE mostrar um autocompletar melhor.
    • Há uma proposta do EcmaScript nessa direção: https://github.com/tc39/proposal-type-annotations
      Acho que isso deveria fazer parte da especificação da linguagem.
  • Fico curioso para ver como o ecossistema NPM vai reagir quando esse recurso deixar de ficar atrás de uma flag e passar a ser o padrão. Não sei se os contribuidores ainda vão compilar versões CJS e ESM ao publicar módulos NPM, ou se simplesmente vão colocar engine: nodejs >= 25 no package.json e pular a etapa de build
    Pessoalmente, se módulos NPM que têm o código-fonte em TS pararem de distribuir dist/.cjs, eu acho melhor, porque fica claro onde colocar o depurador e o console.log. Do ponto de vista de quem contribui para o NPM, a tentação de eliminar a etapa de build também deve ser grande
    Mas, se até uma única dependência distribuir arquivos TS, a premissa de que “todo mundo recebe arquivos TS” pode se espalhar rapidamente. No momento em que o Node.js tirar isso de uma flag experimental opt-in, parece que toda a comunidade passará a esperar implicitamente que os consumidores aceitem arquivos TS
    Se isso acontecer, em poucos meses Firefox e Safari também serão pressionados a aceitar isso, e todos os compiladores JS não terão escolha a não ser descartar anotações de tipo TS
    Pessoalmente, também acho isso aceitável. Afinal, quem usa TS também coloca nos módulos NPM uma etapa de transformação que converte código TS em JS e .d.ts para dar suporte a algum usuário hipotético de JS. Mas, se o Node aceitar arquivos .ts, dá para remover essa etapa de transformação sem perceber; então fico curioso sobre que mecanismo impediria publicadores do NPM de pararem de distribuir arquivos js/d.ts e nem perceberem que quebraram algo

    • Ryan Dahl está criando um novo registro de pacotes chamado JSR para resolver esse problema
      A ideia é permitir publicar código TypeScript sem etapa de build e, quando outro desenvolvedor instalar, ele poder ver o código-fonte TypeScript original, não JavaScript transformado
    • A execução de arquivos .ts dentro de node_modules não é suportada. Essa é uma das principais restrições para não quebrar o ecossistema
    • Bibliotecas TypeScript antigas que ainda mantenho estão sendo transformadas em .cjs e .mjs para npm, e provavelmente vou acabar distribuindo as três versões
      Se fosse um projeto novo escrito do zero, talvez eu distribuísse só TypeScript e não fizesse a transformação
      Correção: segundo um comentário irmão de satanacchio, que parece ser o autor do PR e membro do Comitê Técnico de Direção do Node.js, o suporte a TS é para itens de nível superior e não se aplica a bibliotecas dentro de node_modules
    • Quero publicar meu código-fonte .ts no npm, mas a equipe do TypeScript foi muito contra isso por causa de problemas com tsconfig e desempenho. Ainda assim, estou esperançoso
  • Talvez um dia o Node permita fazer reflexão em tempo de execução desses tipos no JS
    Isso seria um ganho enorme. Em Python, como é possível refletir tipos, ferramentas excelentes como pydantic conseguem gerar validações a partir dos tipos
    Com uma única notação padrão, seria possível obter verificação de tipos, validação de dados em tempo de execução, geração de API e geração de documentação de API
    Hoje, em JS, ferramentas como Zod precisam praticamente reinventar o que o TypeScript já faz, com coisas como const mySchema = z.string();

    • z.string() do Zod não fornece apenas segurança de tipos como o TypeScript. O TypeScript oferece verificação de tipos em tempo de compilação, enquanto o Zod acrescenta validação e parsing em tempo de execução
      z.string() transforma mySchema em um schema funcional que pode fazer parsing e validação. mySchema.parse("some data") passa, e mySchema.parse(321) lança uma exceção
      Usei em lugares que precisavam de validação em tempo de execução e checagens dentro do processo, e funcionou bem; também é possível extrair tipos, como em const A = z.string(); type A = z.infer; // string
      Ou seja, se você define primeiro o tipo no Zod e infere o tipo a partir dele, obtém tanto verificação de tipos em tempo de compilação quanto em tempo de execução. Pode ser exagero para uma base de código pequena e que muda rápido, mas em situações que exigem validação dentro do processo é bem útil e não dá tanto trabalho
    • Hoje o foco da equipe do TypeScript está muito voltado para o alinhamento com a linguagem JavaScript, e novos recursos como tipos em tempo de execução foram praticamente excluídos, ou pelo menos empurrados para depois da proposta de tipos para JavaScript no TC39. O problema de usar decorators em variáveis fora da estrutura de classes foi parecido
      Ainda assim, o TypeScript permite plugins. Eles não são muito usados, mas permitem adicionar à linguagem outros recursos que são transformados no JavaScript resultante
      Um plugin relacionado a tipos em tempo de execução é o Typia, que permite usar assinaturas de tipo do TypeScript em tempo de execução em guards como assert(myValue). Ele intercepta essa chamada de função e gera, no JavaScript transformado, instruções if exaustivas que verificam as características da variável passada
      Não acho que isso vá se tornar parte da linguagem nos próximos 4 a 6 anos, mas já existem bibliotecas que permitem fazer esse tipo de coisa hoje
    • Se algum dia o JS adicionar verificação de tipos, espero que não escolha o TypeScript
      Precisamos de um sistema de tipos sólido de verdade, mas o TS é intencionalmente não sólido. Precisamos de um sistema de tipos que não permita más práticas de codificação como o TS permite, e que force um design capaz de tornar os programas rápidos. Precisamos de um sistema de tipos Hindley-Milner
      Se você quiser transformar um módulo em código tipado, bastaria adicionar "use type". Esse modo deveria proibir partes ruins, como coerção de tipos, e impedir elementos que prejudicam desempenho, como mudar o formato de objetos ou tipos de valores, ou criar arrays misturando valores quaisquer
      Dados vindos de módulos sem tipos seriam convertidos ou, se não pudessem ser convertidos, gerariam erro; assim o compilador poderia otimizar profundamente o código tipado com garantias de tipo mais fortes e sem risco de bailout no meio do caminho
    • Fico curioso se isso significa que o Node conseguiria saber se uma exceção é uma subclasse de ValueError, ou se um objeto é uma instância de SomeClass. Sou iniciante em TS, então eu achava que, fora arrays, objetos, números e strings, não havia tipos em JS, e que Zod ou funções de type guard retornavam objetos comuns no nível de “confia em mim”
  • A experiência de desenvolvimento do Bun é algo bastante sem precedentes nessa área, e agora a maioria dos casos de uso está coberta, ou o Bun não trava ao usar scripts de execução com bun run
    Por outro lado, no Node não dá para configurar os imports para não exigirem extensão, nem fazer o tsc acrescentar automaticamente a extensão .js ao resultado da compilação, então é preciso adicionar um bundler
    O suporte nativo a TypeScript deve corrigir bastante esse pequeno incômodo, mas, mesmo quando estiver estável, é difícil imaginar que vá igualar o Bun em experiência de usuário ou desempenho

    • As extensões devem ser exigidas. Não dá para fazer busca de caminhos na rede como em um disco local, e runtimes conectados à rede, como o navegador, são muito importantes em JavaScript
    • Gosto muito do Bun, mas o Deno ainda é mais maduro, mais estável e tem mais recursos. Tem coisas como workers estáveis, HTTP/2, e, dependendo do caso de uso, o V8 pode ser mais rápido que o JSC
      A experiência de desenvolvimento e as ferramentas também são de altíssimo nível. O Deno também consegue fazer verificação de tipos e, se me lembro bem, inclui o TSC
      O Bun gera muito burburinho, mas, para trabalho sério, hoje o Deno é claramente uma opção melhor. Ainda assim, a visão e a execução do Bun são impressionantes e isso é bom para os desenvolvedores
    • Tentei usar o Bun duas vezes, com alguns meses de intervalo, mas no Windows a execução de bun install falhou e nunca consegui fazê-lo funcionar direito: https://github.com/search?q=repo%3Aoven-sh%2Fbun+bun+install...
    • “Sem precedentes” talvez, se você excluir o Deno
    • Acho que o Bun ainda está cru demais. Foi feito em Zig, que nem chegou à 1.0
  • Há muito tempo eu queria um runtime TypeScript e também gosto muito de TypeScript, mas é engraçado que, depois de ter deixado Java há tanto tempo, acabei procurando algo muito mais próximo de Java
    No fim, parece que o que queríamos era Java com JIT, um sistema de tipos mais rico e tipagem gradual. O ecossistema npm também tem muitos pontos negativos, mas usar bibliotecas nesse ecossistema é muito menos trabalhoso e mais divertido
    Surpreendentemente, Rust está na outra ponta do espectro das linguagens, mas às vezes passa uma sensação parecida
    Correção: a expressão JIT não foi adequada. Escrevi JIT por preguiça, mas queria me referir à diferença de tempo de inicialização e de runtime ao executar algo na JVM e no V8. Java parece pesado, enquanto o ecossistema JavaScript parece muito mais ágil

    • Como Java foi literalmente o que popularizou o termo JIT, não sei o que significa “Java com JIT”
      Também não sei de jeito nenhum se TypeScript é mais próximo de Java. Acho que a única coisa que os dois compartilham é o nome incorreto “Javascript” e suporte a programação imperativa; o restante é muito diferente
    • Pela minha experiência, TypeScript é muito melhor que Java. É muito menos verboso e mais flexível
    • Pessoalmente, prefiro muito mais usar TypeScript do que Java. Especialmente se você evita as partes um tanto imperfeitas das classes e se concentra em interfaces e funções, a usabilidade fica muito melhor
      Porém, algo que existe em Java e de que sinto falta em TypeScript é reflexão de tipos em runtime. No ecossistema TypeScript há inúmeras formas de contornar isso, mas todas me parecem um pouco feias
    • Java tem JIT. E fico curioso para saber de que forma o sistema de tipos do TypeScript é mais rico em recursos que o de Java
    • Também estou em uma posição parecida. Pessoalmente, o que eu quero é a biblioteca padrão e o ecossistema de C#, removendo as partes legadas atuais ou praticamente abandonadas, com a facilidade e a força de tipos algébricos estruturais
      AoT também estaria bom, e seria ótimo ter uma opção de binário único. Idealmente, sem runtime e com eliminação inteligente. Seria ainda melhor se também rodasse no navegador com JIT
      Também quero conveniências de compilador e verificador de tipos, como pattern matching exaustivo
  • Um recurso do Deno de que gosto está chegando diretamente ao Node. Muito bom
    Talvez agora eu não precise sempre instalar esbuild para remover tipos. Estou animado porque parece que vai ficar muito mais fácil escrever e usar scripts em TypeScript
    Recentemente tenho preferido Python para scripts descartáveis, mas, em termos de tipos, pessoalmente acho TypeScript melhor que Python. Especialmente scripts grandes revisitados alguns meses depois se beneficiam muito dos tipos

  • Foi um mês realmente movimentado para o Node. Primeiro, na v22.5.0, foi adicionado o node:sqlite, e agora o suporte a TypeScript também está chegando. Gosto da direção que o Node está tomando

    • Acho que é por influência ou concorrência do Bun. É bom para todo mundo
    • O test runner adicionado recentemente também é muito legal
  • Sou o autor do PR. Podem perguntar qualquer coisa

    • Fico curioso para saber quais são os próximos passos e que futuro você vê para o TypeScript no Node e no ecossistema JS como um todo
  • Comecei a usar Node.js em trabalhos de backend muito tempo atrás, e ele parecia ter muitas vantagens em relação a escrever código em PHP, sem trazer muitos dos problemas do Java
    Mas o Node era um tanto bruto, e parecia uma linguagem em que era preciso acoplar um monte de coisas para criar a linguagem que eu queria. No fim, comecei a usar Go, que era muito mais fácil de usar e, embora às vezes fosse muito mais verboso, simplificava a programação graças à segurança de tipos
    TypeScript parecia uma boa opção, mas no fim era mais um remendo. Não sei bem que valor se ganha usando TypeScript em vez de Go. Ter tipos explícitos é bom, mas não resolve outros problemas da linguagem que já foram resolvidos em Go. O Deno também resolve parte deles
    A grande vantagem do Node sobre Go é a possibilidade de prototipar rapidamente, mas usar TypeScript parece reduzir bastante essa vantagem. Por isso é difícil julgar se isso é um bom avanço ou se faz o Node perder as características que o tornavam uma boa opção

    • TypeScript é JS mais seguro. Mesmo usando TS, você continua escrevendo JS
      Pelo uso da expressão “remendo”, talvez o problema seja menos a linguagem em si e mais a ausência de um framework opinativo como o Django, que gerencia tudo por padrão. Gosto de usar Django, mas é um pouco mais difícil sair do caminho prescrito
    • A resposta óbvia é a familiaridade com a linguagem. Se o código de frontend do projeto é JavaScript ou TypeScript, usar Node é uma escolha fácil. Dá para usar bibliotecas compartilhadas, tipos compartilhados etc.
    • Pode ser porque, se você já conhece JavaScript ou tem experiência interna com ele, isso é melhor do que aprender Go. Nós também tínhamos experiência anterior em frontend e estamos usando com cdktf; pareceu uma escolha mais lógica do que Go