1 pontos por GN⁺ 2023-07-09 | 1 comentários | Compartilhar no WhatsApp
  • Uma lista que defende a necessidade de o TypeScript emitir informações de tipo em runtime e reúne projetos de Type Mapping, Code Generation / External Tool e Adapter que contornam esse problema
  • O problema central é que, ao lidar com serialização e validação sem um sistema de tipos reflexivo, acaba sendo necessário escrever um volume interminável de boilerplate ou fazer geração de código bespoke baseada em arquivos de esquema
  • Como alternativas, são citados io-ts, zod e outros; o inconveniente é que é preciso redeclarar os tipos no formato específico de cada biblioteca, e essas bibliotecas não conseguem dar suporte a todos os recursos de tipos do TypeScript
  • Embora reconheça como vantagem o apagamento de tipos do TypeScript, que permite que projetos JavaScript usem o JavaScript emitido sem conhecimento de TypeScript, o texto argumenta que seria possível emitir informações de tipo em forma de tabelas de consulta separadas do código
  • Com um pedido para que isso não seja resolvido com decorators, propõe abordagens como uma função de ordem superior reconhecida pelo compilador, como typescript.generateRuntimeType<T>(), Type Providers do F# e Source Generators do C#, para dar suporte ao uso de interfaces e a tipos de bibliotecas externas
  • Liga uma discussão relacionada em uma issue do GitHub de 8 anos atrás e pede que, se um projeto tiver enfrentado o mesmo problema, envie um PR para adicioná-lo à lista, independentemente do número de estrelas

1 comentários

 
GN⁺ 2023-07-09
Opiniões no Hacker News
  • Precisei ler a descrição do problema umas 4 vezes para entender o que estava sendo pedido, e casos assim mostram por que escrever de forma concisa é importante
    O pedido real parece ser segurança de tipos em tempo de execução, mas o TypeScript sempre traçou a linha de que não pretende se tornar um runtime substituto/adicional para JavaScript, então isso parece pouco provável. O papel do TypeScript é compilar para JS e sair de cena; o que acontece depois no V8 e afins fica fora do escopo.
    O pedido de que “o TypeScript deve emitir informações de tipo em runtime” está mais próximo de pedir um produto novo bem diferente do TypeScript atual

    • Mais do que pedir segurança de tipos em tempo de execução em si, parece um pedido para refletir tipos em tempo de compilação, gerar valores e permitir usar essas informações em runtime
      Por exemplo, seria bom poder criar uma função validate genérica que receba uma interface e um objeto arbitrários e faça a validação. Talvez fosse possível gerar código JS de validação por tipo via reflexão em tempo de compilação, ou passar T como argumento em runtime para comparar, mas acho que isso é difícil dentro da filosofia atual do TypeScript
    • Só pelo título já percebi que era um recurso que eu queria havia muito tempo, e está mais próximo de pedir algo como https://github.com/rbuckton/reflect-metadata, só que mais oficial e bem suportado
      O TypeScript sabe muita coisa sobre os tipos durante a compilação, mas descarta essas informações quando a compilação termina. Mesmo podendo exportá-las para um arquivo ou deixá-las como metadados em Reflect, descartá-las é uma limitação lamentável, especialmente considerando a natureza do JavaScript, em que “tudo é objeto”.
      Mesmo que não se obtenha segurança de tipos exata do TS, dá para adicionar algumas verificações com getters/setters ou Proxy do ES2015; deixar informações de tipo em runtime abriria várias possibilidades interessantes como metadados executáveis
    • Não é correto dizer que é impossível porque o TypeScript é um compilador. Bastaria dar suporte a uma biblioteca de reflexão que emita informações de tipo como objetos JS e permita consultá-las em runtime
      O enum do TS já é emitido como objeto JS e pode ser consultado em runtime, mas tipos union de literais de string não. Além disso, o TypeScript oferece suporte a funções de type guard, então não parece tão difícil gerar automaticamente esse tipo de função usando informações presentes no sistema de tipos
    • Mesmo passando os olhos por alto, o pedido estava claro. É para o TypeScript emitir as informações de tipo que descobriu durante o processo de apagamento de tipos por um canal adicional ao lado do JavaScript gerado
      Dá para pensar na analogia de arquivos PDB. Como são informações que o TypeScript já tem e depois descarta, não é preciso um produto completamente novo
    • No topo do README há um link para uma “issue do GitHub de 7 anos atrás”, onde o problema é explicado de forma mais direta
      https://github.com/microsoft/TypeScript/issues/3628
  • Do ponto de vista de um PM do TypeScript, entendo a necessidade. Validação de dados frequentemente exige checagem de tipos em tempo de execução, e há muitas bibliotecas para cobrir essa lacuna
    Ainda assim, o próprio fato de existirem muitas bibliotecas com decisões de design diferentes é um sinal de que isso não é um problema resolvido com uma resposta óbvia. Isso já era conhecido no design inicial do TypeScript, e acho que esse princípio se sustentou bem.
    Em vez disso, o TypeScript ficou poderoso o bastante para expressar com precisão, em tipos, o que bibliotecas de checagem de tipos em runtime realmente fazem, e usuários conseguem compor lógica de validação em runtime a partir dos tipos por meio de APIs. Esse nível parece uma flexibilidade razoável

    • Comecei a programar há pouco tempo e fico me perguntando por que o TypeScript não consegue criar tipos de dados definidos pelo usuário mais sofisticados
      Por exemplo, imagino uma linguagem em que seja possível definir como tipo um número dentro de um determinado intervalo ou uma string que satisfaça um padrão de CEP, e em que o compilador use uma função de validação escrita como uma função comum para verificar a validade. Fico curioso se a ausência desse recurso é uma decisão de design para evitar complexidade desnecessária ou uma limitação técnica, como desempenho
    • Um plugin de pré-processador oficial no compilador TypeScript poderia ajudar
      Mesmo hoje, quem precisa pode criar um pré-processador que gere objetos de runtime a partir de informações de tipo antes de passar tudo para o tsc, mas os esforços ficam espalhados. Com um pré-processador oficial plugável e um ecossistema, boas soluções poderiam ser descobertas
    • Se a Microsoft hospedasse o MacroScript como plugin ou wrapper de nível superior para TypeScript, isso poderia resolver o problema
      Bastaria acender a faísca para a comunidade ajudar na manutenção, e isso atenderia a várias necessidades de geração de código, da geração de clientes a asserções de tipos em runtime
    • Gostaria de saber o que acham de outra proposta: deixar as informações de tipo em objetos Class após a compilação
  • Há motivos para não fazer isso. O TypeScript acabaria virando algo como um runtime em cima do JavaScript, e uma nova linguagem compilada para JS
    Hoje, o TypeScript é mais próximo de JavaScript com anotações de tipo. Já existem muitas linguagens que compilam para JS, então dá para usar uma delas. Pessoas que querem TypeScript com tipos em runtime parecem querer escrever código mais no estilo Java/OOP do que JavaScript, mas JavaScript é uma linguagem de tipagem dinâmica, e isso também é uma vantagem

    • Não necessariamente. Se uma macro como generateTypeInfo!() se expandisse para um objeto JS que codifica o tipo Foo, ainda compilaria para JavaScript legível
      Só que essa macro quebraria a propriedade de que “TS = JS com anotações de tipo, e compilar é só remover as anotações”. Isso porque seria preciso calcular de fato o tipo estrutural de Foo.
      Além disso, como os tipos do TypeScript são estruturais, ao inspecionar a estrutura de um objeto em runtime dá para inferir o tipo em certa medida; mas, se você exigir informações nominais apagadas, como saber se pertence a uma união de strings, ou exigir nomes de tipos, isso rapidamente vira um problema difícil por causa da completude de Turing do sistema de tipos do TypeScript e das conversões estruturais implícitas
    • Dá para ter as duas coisas. Dá para imaginar um mundo em que código e dados ficam próximos, como na homoiconicidade do Lisp
      Falando sério, mesmo sem runtime, se você expuser tipos como dados, obtém reflexão. Vendo quantos desenvolvedores se esforçam para imitar isso, não fazer parece até tolice
    • Não entendo bem essa lógica. TypeScript já é uma nova linguagem de superconjunto que compila para JS
      Tipos em runtime resolvem o problema de ter que manter duplicados, separadamente, um sistema de tipos para compilação e outro para validação de dados, e isso não parece ter relação direta com OOP. O io-ts, uma biblioteca preferida para contornar esse problema, também se apoia fortemente em programação funcional
    • Em projetos grandes o suficiente, linguagens de tipagem dinâmica não são boas; são quase um desastre emaranhado em que você continua tropeçando em valores inesperados como NaN
      Nos workflows que vi, TypeScript já é uma linguagem compilada para JS, então acho melhor aproveitar esse benefício ao máximo
    • TypeScript não é simplesmente JavaScript com anotações de tipo. Pode ser um dos modos, mas ele também consegue emitir estruturas antigas de JS, o que pode fazer o código parecer completamente diferente do TS original
      Ele é mais próximo de anotações de tipo + Babel, e adicionar a isso um recurso muito útil é uma boa ideia. É estranho não ser possível transformar com segurança uma string vinda de JSON.parse em uma estrutura tipada; outras linguagens em geral permitem isso
  • Antes do que vem abaixo, este é um tema válido, com bastante espaço para debate, e não acho que exista objetivamente uma única resposta correta
    O TypeScript é uma camada opcional sobre o JavaScript, exceto pela exceção de Enum emitir objetos, e código TS vira JS se você apenas remover os tipos. Se não fugirmos muito desse princípio, a demanda real se parece mais com uma biblioteca que crie serializadores/validadores a partir de tipos. Já existem muitas bibliotecas assim, e no fim parece uma demanda para tornar uma delas a opção oficial padrão.
    Pessoalmente, não quero reflexão em runtime entrando na linguagem central do TypeScript. Em runtime, quero que exista apenas JavaScript, e valorizo o fato de o JS emitido ser legível e depurável mesmo sem sourcemaps

    • Além de Enum, há outras exceções que emitem código em runtime, e a maioria também é considerada erro. Exemplos clássicos eram module e namespace, que eram estruturas de runtime
      Nos últimos anos, porém, isso foi usado principalmente pelo próprio TypeScript, e o TypeScript também tem se afastado disso recentemente. Outra exceção bastante popular são as propriedades de parâmetro, uma sintaxe para definir tipos de membros de classe nos parâmetros do construtor; ela parece ter escapado de menos controvérsia por reduzir boilerplate duplicado
    • Dizer que “código TS vira JS sem transformação” é algo plausível se você encarar a linguagem inteira e a notação de tipos dessa forma
  • Em vez de implorar aos deuses do TypeScript, talvez seja melhor negociar do lado do JavaScript para colocar verificação de tipos no JS
    Se precisar de tipos em runtime, use type guards; se precisar disso com frequência, use io-ts ou zod e escreva tipos como validadores/codecs/schemas. Até que exista uma forma consensual em JavaScript, não acho que a especificação do TS, o type checker e a comunidade devam assumir a validação em runtime

    • Não é perfeito, mas estou bem satisfeito com a abordagem de usar schemas e validadores com zod e derivar os tipos a partir deles. Gosto do fato de a validação evoluir junto com as mudanças de tipo
      Algumas pessoas sentem que esse tipo de recurso deveria entrar na linguagem, mas há muitas escolhas de design controversas entre as bibliotecas, então pode ser melhor escolher a implementação de acordo com as necessidades do projeto.
      Dito isso, padrões como validação em runtime com zod e derivação de tipos baseada em schema nem sempre combinam bem com pattern matching do ts-pattern. As definições de tipo dessas bibliotecas são um labirinto, e às vezes um código que parece que deveria funcionar não se comporta como esperado. Seria muito bom se segurança em runtime e pattern matching com verificação de exaustividade se integrassem de forma fluida
    • Tenho uma grande dúvida se realmente seria bom colocar um recurso desses no JavaScript
      Sinto que Promise, decoradores e o novo operador de pipe seguiram todos por uma direção de implementação errada
  • Lembro que um dos desenvolvedores do TypeScript disse que, se fosse começar de novo, não teria colocado enum. Isso porque enum é o único recurso que emite código em runtime
    TypeScript não altera o comportamento em runtime e não tem nada especial específico de TypeScript como {#if}

    • Isso é verdade. Não sei se esse foi o único motivo, mas certamente foi um deles
      Ainda assim, o TypeScript tem alguns recursos que vão além de “JavaScript + anotações de tipo”. namespace, a sintaxe para definir propriedades de classe em argumentos do construtor, os antigos decoradores experimentais, a sintaxe de parâmetro this, que desaparece na compilação mas parece mais do que apenas uma anotação de tipo em uma função JS, e assim por diante
  • Um título melhor seria algo mais próximo de “TypeScript, por favor forneça reflexão/tipos em runtime”
    A melhor solução atual provavelmente parece ser emitDecoratorMetadata. https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata

  • O autor parece ter entendido mal os objetivos de design do TypeScript. O objetivo não é gerar uma saída JS limpa e sem complexidade, mas sim garantir que a semântica em tempo de execução do TypeScript permaneça idêntica à do JavaScript
    Tirando a lamentável exceção de enum, TypeScript vira JavaScript apenas removendo as anotações de tipo. O ecossistema ao redor do TypeScript depende de apagamento completo de tipos, e, se essa exigência fosse aceita, o suporte a TS no ESBuild, Deno e Bun poderia se tornar praticamente impossível. Isso porque cada um teria de reimplementar o tsc inteiro em sua própria linguagem.
    Por outro lado, as bibliotecas complexas das quais o OP reclama são implementadas no espaço do usuário, então são compatíveis com essas ferramentas

    • Há muitas maneiras de emitir informações de tipo em tempo de execução estaticamente, mesmo sem runtime. Dar para transformar keyof em uma lista de chaves de uma classe, ou fazer com que classes TS inicializem chaves como undefined, como classes ES6
      Hoje, as classes TypeScript removem todas as chaves que não foram definidas explicitamente, então, se você chamar Object.keys() em uma nova instância, nada aparece. Só de transformar classes TS como classes ES6 por meio de uma opção no tsconfig.json, ou de permitir classes ES6 dentro do TS, a geração de código já ficaria muito mais fácil.
      Além disso, seria excelente se uma sintaxe estática como tstypeof Foo::bar compilasse para "string". Mesmo uma RTTI básica já poderia eliminar imediatamente muito código TypeScript boilerplate e confuso
  • Fico feliz em ver este texto. Comecei com TypeScript em 2018 e, depois de uns 2 anos, comecei a acreditar que ele, por natureza, não é uma solução completa
    Vim de Haskell/C#/F#, e, mesmo com um sistema de tipos poderoso, TS não oferece muitos dos benefícios que essas linguagens dão, exceto em parte durante o desenvolvimento. Ao lidar com o mundo real, ele é muito mais limitado na prática do que C#. Se você não souber que, depois da compilação, ele cai para JS sem verificações, precisa estar sempre consciente das abstrações vazadas para evitar bugs que deveriam ser impossíveis em uma ferramenta que se diz estaticamente tipada

    • O propósito do TypeScript sempre foi rodar na web, não dar uma superioridade de linguagem em relação ao C#
      Se você precisa desenvolver em .NET, usa C#; se precisa desenvolver para a web, usa TypeScript. C# tem tipagem nominal e genéricos reificados bastante bons; TypeScript tem tipagem estrutural e, em troca de abrir mão da solidez, possui um sistema de tipos poderoso para expressar relações de tipos complexas.
      Duvido que uma linguagem que juntasse as duas fosse boa; as duas funcionam bem em direções diferentes, mas essas direções não combinam muito entre si
  • Para validação de dados em tempo de execução, usei https://zod.dev e fiquei totalmente satisfeito
    Achei bem legal poder expressar a intenção ali mesmo com uma API fluente, sem definir separadamente um tipo nominal distinto

    • Tipos nominais têm outros usos
      Um exemplo simples: eles são necessários para diferenciar, no domínio, o número 2 do valor monetário 2