Segurança de tipo em JavaScript: Flow e TypeScript

Qualquer pessoa que lida com desenvolvimento de IU em uma empresa sangrenta provavelmente já ouviu falar de "JavaScript digitado", que significa "TypeScript da Microsoft". Mas além desta solução, existe pelo menos mais um sistema de tipagem JS comum, e também de um grande player no mundo da TI. Este é um fluxo do Facebook. Por causa da minha aversão pessoal pela Microsoft, costumava usar sempre o flow. Objetivamente, isso foi explicado pela boa integração com utilitários existentes e facilidade de transição.



Infelizmente, devemos admitir que em 2021 o flow já é significativamente inferior ao TypeScript, tanto em popularidade quanto no suporte de uma variedade de utilitários (e bibliotecas), e é hora de enterrá-lo na prateleira e parar de mastigar cactos.vá para o padrão TypeScript de fato. Mas, por último, gostaria de comparar essas tecnologias, digamos um par (ou não) do fluxo do Facebook.



Por que você precisa de segurança de tipo em JavaScript?



JavaScript é uma linguagem maravilhosa. Não, não gosto disso. O ecossistema construído em torno do JavaScript é ótimo. Para 2021, ela realmente admira o fato de que você pode usar os recursos mais modernos da linguagem e, em seguida, alterando uma configuração do sistema de compilação, transpilar o arquivo executável para suportar sua execução em versões anteriores de navegadores, incluindo o IE8 , não será de noite, lembre-se. Você pode "escrever em HTML" (ou seja, JSX) e, em seguida, usar o utilitário babel



(ou tsc



) substituir todas as tags por construções JavaScript corretas, como chamar a biblioteca React (ou qualquer outra, mas mais sobre isso em outro post).



Por que o JavaScript é uma boa linguagem de script que roda em seu navegador?



  • JavaScript não precisa ser "compilado". Você apenas adiciona construções JavaScript e o navegador deve entendê-las. Isso imediatamente dá um monte de coisas convenientes e quase gratuitas. Por exemplo, a depuração diretamente no navegador, que não é responsabilidade do programador (que não deve esquecer, por exemplo, de incluir um monte de opções de depuração do compilador e bibliotecas correspondentes), mas do desenvolvedor do navegador. Você não precisa esperar 10-30 minutos (tempo real para C / C ++) enquanto seu projeto de linha de 10k é compilado para tentar escrever algo diferente. Basta alterar a linha, recarregar a página do navegador e observar o novo comportamento do código. E no caso de usar, por exemplo, webpack, a página também será recarregada para você. Muitos navegadores permitem que você altere o código dentro da página usando seus devtools.
  • - . 2021 . Chrome/Firefox, , , 5% (enterprise-) 30% (UI/) , .
  • JavaScript , . — ( worker'). , 100% CPU ( UI ), , , Promise/async/await/etc.
  • Ao mesmo tempo, nem mesmo considero a questão de por que o JavaScript é importante. Afinal, com a ajuda do JS, você pode: validar formulários, atualizar o conteúdo da página sem recarregá-lo totalmente, adicionar efeitos de comportamento não padrão, trabalhar com áudio e vídeo, e você pode até mesmo escrever todo o cliente de seu aplicativo corporativo em JavaScript.


Como acontece com quase qualquer linguagem de script (interpretada), em JavaScript você pode ... escrever código quebrado. Se o navegador não alcançar esse código, não haverá mensagem de erro, nenhum aviso, nada. Por um lado, isso é bom. Se você tiver um site muito grande, mesmo um erro de sintaxe no código do manipulador de clique de botão não deve fazer com que o site não seja totalmente carregado pelo usuário.



Mas, claro, isso é ruim. Porque o próprio fato de ter algo não funcionando em algum lugar do site é ruim. E seria ótimo, antes que o código chegue a um site de trabalho, verificar todos os scripts no site e certificar-se de que pelo menos compilem. E idealmente - e trabalhar. Para isso, uma variedade de conjuntos de utilitários são usados ​​(meu conjunto favorito é npm + webpack + babel / tsc + karma + jsdom + mocha + chai).



Se vivemos em um mundo ideal, então todos os scripts em seu site, mesmo os de uma linha, são cobertos com testes. Mas, infelizmente, o mundo não é ideal, e para toda aquela parte do código que não é coberta pelos testes, só podemos contar com algum tipo de ferramenta de verificação automatizada. Que pode verificar:



  • JavaScript. , JavaScript, , , . /// .
  • . , , . , :



    var x = null;
    x.foo();
    
          
          





    . — null .


Além dos erros de semântica, pode haver erros ainda mais terríveis: erros lógicos. Quando o programa é executado sem erros, mas o resultado não é o esperado. Clássico com adição de strings e números:



console.log( input.value ) // 1
console.log( input.value + 1 ) // 11

      
      





As ferramentas de análise de código estático existentes (eslint, por exemplo) podem tentar rastrear um número significativo de erros potenciais que um programador comete em seu código. Por exemplo:





Observe que todas essas regras são essencialmente restrições que o linter impõe ao programador. Ou seja, o linter realmente reduz os recursos da linguagem JavaScript para que o programador cometa menos erros potenciais. Se você habilitar todas as regras, será impossível fazer atribuições em condições (embora o JavaScript inicialmente permita isso), use chaves duplicadas em literais de objeto e nem mesmo pode ser chamado console.log()



.



Adicionar tipos de variáveis ​​e verificação de tipo de chamadas são limitações adicionais da linguagem JavaScript para reduzir possíveis erros.



imagem

Tentando multiplicar um número por uma string



Uma tentativa de acessar uma propriedade inexistente (não descrita no tipo) de um objeto

Uma tentativa de acessar uma propriedade inexistente (não descrita no tipo) de um objeto.



Uma tentativa de acessar uma propriedade inexistente (não descrita no tipo) de um objeto

Uma tentativa de chamar uma função com um tipo de argumento incompatível.



Se escrevermos este código sem um verificador de tipo, o código será transpilado com êxito. Nenhum meio de análise de código estático, se não usar (explícita ou implicitamente) informações sobre os tipos de objetos, não será capaz de encontrar esses erros.



Ou seja, adicionar digitação ao JavaScript adiciona restrições adicionais ao código que o programador grava, mas permite que você encontre erros que poderiam ocorrer durante a execução do script (ou seja, provavelmente no navegador do usuário).



Capacidades de digitação de JavaScript



Fluxo TypeScript
Capacidade de definir o tipo de uma variável, argumento ou tipo de retorno de uma função
a : number = 5;
function foo( bar : string) : void {
    /*...*/
} 

      
      



Capacidade de descrever seu tipo de objeto (interface)
type MyType {
    foo: string,
    bar: number
}

      
      



Restringindo valores para um tipo
type Suit = "Diamonds" | "Clubs" | "Hearts" | "Spades";

      
      



Extensão de nível de tipo separada para enumerações
enum Direction { Up, Down, Left, Right }

      
      



Tipos de "adição"
type MyType = TypeA & TypeB;

      
      



"Tipos" adicionais para casos complexos
$Keys<T>, $Values<T>, $ReadOnly<T>, $Exact<T>, $Diff<A, B>, $Rest<A, B>, $PropertyType<T, k>, $ElementType<T, K>, $NonMaybeType<T>, $ObjMap<T, F>, $ObjMapi<T, F>, $TupleMap<T, F>, $Call<F, T...>, Class<T>, $Shape<T>, $Exports<T>, $Supertype<T>, $Subtype<T>, Existential Type (*)
      
      



Partial<T>, Required<T>, Readonly<T>, Record<K,T>, Pick<T, K>, Omit<T, K>, Exclude<T, U>, Extract<T, U>, NonNullable<T>, Parameters<T>, ConstructorParameters<T>, ReturnType<T>, InstanceType<T>, ThisParameterType<T>, OmitThisParameter<T>, ThisType<T>

      
      





Ambos os mecanismos para suporte ao tipo JavaScript têm aproximadamente os mesmos recursos. No entanto, se você vem de linguagens fortemente tipadas, mesmo o JavaScript tipado tem uma diferença muito importante do Java: todos os tipos descrevem essencialmente interfaces, ou seja, uma lista de propriedades (e seus tipos e / ou argumentos). E se duas interfaces descrevem as mesmas propriedades (ou compatíveis), elas podem ser usadas no lugar uma da outra. Ou seja, o código a seguir está correto em JavaScript digitado, mas claramente incorreto em Java ou, digamos, C ++:



type MyTypeA = { foo: string; bar: number; }
type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar : MyTypeA = { foo: "World", bar: 42 } as MyTypeA;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Este código está correto do ponto de vista do JavaScript digitado, uma vez que a interface MyTypeB requer uma propriedade foo



com um tipo string



, enquanto uma variável com a interface MyTypeA o faz.



Este código pode ser reescrito um pouco mais curto, usando uma interface literal para uma variável myVar



.



type MyTypeB = { foo: string; }

function myFunction( arg : MyTypeB ) : string {
    return `Hello, ${arg.foo}!`;
}

const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





O tipo de variável myVar



neste exemplo é uma interface literal { foo: string, bar: number }



. Ele ainda é compatível com a interface esperada de um argumento de arg



função myFunction



, portanto, esse código está livre de erros do ponto de vista de, por exemplo, TypeScript.



Esse comportamento reduz significativamente o número de problemas ao trabalhar com diferentes bibliotecas, código personalizado e até mesmo apenas chamar funções. Um exemplo típico é quando alguma biblioteca define opções válidas e as passamos como um objeto de opções:



// -  
interface OptionsType {
    optionA?: string;
    optionB?: number;
}
export function libFunction( arg: number, options = {} as OptionsType) { /*...*/ }

      
      





//   
import {libFunction} from "lib";
libFunction( 42, { optionA: "someValue" } );

      
      





Observe que o tipo OptionsType



não é exportado da biblioteca (nem é importado para o código personalizado). Mas isso não impede que você chame a função usando a interface literal para o segundo argumento da options



função e para o sistema de digitação - para verificar a compatibilidade de tipo deste argumento. Tentar fazer algo assim em Java causará uma confusão clara entre o compilador.



Como funciona do ponto de vista do navegador?



Nem o TypeScript da Microsoft nem o fluxo do Facebook são compatíveis com os navegadores. Bem como as extensões de linguagem JavaScript mais recentes ainda não encontraram suporte em alguns navegadores. Então, em primeiro lugar, como esse código é verificado quanto à exatidão e, em segundo lugar, como ele é executado pelo navegador?



A resposta é trapacear. Todo código JavaScript "não padrão" passa por um conjunto de utilitários que transformam o código "não padrão" (desconhecido para os navegadores) em um conjunto de instruções que os navegadores entendem. E para a digitação, toda a "transformação" consiste no fato de que todos os refinamentos de tipo, todas as descrições de interface, todas as restrições do código são simplesmente removidas. Por exemplo, o código do exemplo acima se transforma em ...



/* : type MyTypeA = { foo: string; bar: number; } */
/* : type MyTypeB = { foo: string; } */

function myFunction( arg /* : : MyTypeB */ ) /* : : string */ {
    return `Hello, ${arg.foo}!`;
}

const myVar /* : : MyTypeA */ = { foo: "World", bar: 42 } /* : as MyTypeA */;
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Essa.

function myFunction( arg ) {
    return `Hello, ${arg.foo}!`;
}
const myVar = { foo: "World", bar: 42 };
console.log( myFunction( myVar ) ); // "Hello, World!"

      
      





Essa conversão geralmente é feita de uma das seguintes maneiras.





Exemplos de configurações de projeto para fluxo e TypeScript (usando tsc).

Fluxo TypeScript
webpack.config.js
{
  test: /\.js$/,
  include: /src/,
  exclude: /node_modules/,
  loader: 'babel-loader',
},

      
      



{
  test: /\.(js|ts|tsx)$/,
  exclude: /node_modules/,
  include: /src/,
  loader: 'ts-loader',
},

      
      



Configurações do transpiler
babel.config.js tsconfig.json
module.exports = function( api ) {
  return {
    presets: [
      '@babel/preset-flow',
      '@babel/preset-env',
      '@babel/preset-react',
    ],
  };
};

      
      



{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": false,
    "jsx": "react",
    "lib": ["dom", "es5", "es6"],
    "module": "es2020",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "outDir": "./dist/static",
    "target": "es6"
  },
  "include": ["src/**/*.ts*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

      
      



.flowconfig
[ignore]
<PROJECT_ROOT>/dist/.*
<PROJECT_ROOT>/test/.*
[lints]
untyped-import=off
unclear-type=off
[options]

      
      





A diferença entre as abordagens babel + strip e tsc é pequena em termos de montagem. No primeiro caso, usa-se babel, no segundo, será tsc.





Mas há uma diferença se um utilitário como o eslint for usado. O TypeScript para linting com eslint tem seu próprio conjunto de plug-ins que permitem encontrar ainda mais bugs. Mas exigem que na hora da análise pelo linter ele tenha informações sobre os tipos de variáveis. Para fazer isso, apenas tsc deve ser usado como analisador de código, não babel. Mas se o tsc for usado para o linter, será errado usar o babel para a construção (o zoológico de utilitários usados ​​deve ser mínimo!).





Fluxo TypeScript
.eslint.js
module.exports = {
  parser: 'babel-eslint',
  parserOptions: {
    /* ... */

      
      



module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    /* ... */

      
      





Tipos para bibliotecas



Quando uma biblioteca é publicada no repositório npm, é a versão JavaScript que é publicada. Presume-se que o código publicado não precisa ser modificado para ser usado em um projeto. Ou seja, o código já passou pela traspilation necessária via babel ou tsc. Mas então as informações sobre os tipos no código já foram perdidas. O que fazer?



No fluxo, presume-se que, além da versão "pura" do JavaScript, a biblioteca conterá arquivos com a extensão .js.flow



contendo o código de fluxo de origem com todas as definições de tipo. Então, ao analisar o fluxo, poderá conectar esses arquivos para verificação de tipo, e na construção do projeto e sua execução, eles serão ignorados - serão utilizados arquivos JS comuns. Você pode adicionar arquivos .flow à biblioteca por meio de uma cópia simples. No entanto, isso aumentará significativamente o tamanho da biblioteca em npm.



No TypeScript, não é sugerido manter os arquivos de origem lado a lado, mas apenas uma lista de definições. Se houver um arquivo myModule.js



, ao analisar o projeto, o TypeScript procurará um arquivo próximo myModule.js.d.ts



, no qual espera ver as definições (mas não o código!) De todos os tipos, funções e outras coisas que são necessárias para analisar os tipos. O transpiler tsc é capaz de criar tais arquivos a partir do TypeScript de origem por conta própria (veja a opção declaration



na documentação).



Tipos para bibliotecas legadas



Para fluxo e TypeScript, há uma maneira de adicionar declarações de tipo para as bibliotecas que não contêm inicialmente essas descrições. Mas isso é feito de maneiras diferentes.



Para o fluxo, não existe um método “nativo” suportado pelo próprio Facebook. Mas existe um projeto com tipo de fluxo que coleta essas definições em seu repositório. Na verdade, uma forma paralela para o npm criar versões de tais definições, e também uma forma "centralizada" de atualização não muito conveniente.



No TypeScript, a maneira padrão de escrever tais definições é publicá-las em pacotes npm especiais com o prefixo "@types"... Para adicionar uma descrição dos tipos de uma biblioteca ao seu projeto, basta conectar a @tipos-library correspondente, por exemplo, @types/react



para React ou @types/chai



para chai.



Comparação de fluxo e TypeScript



Uma tentativa de comparar o fluxo e o TypeScript. Os fatos selecionados são coletados do artigo "TypeScript VS Flow" de Nathan Sebhastian, alguns são coletados independentemente.



Suporte nativo em várias estruturas. Native - nenhuma abordagem adicional com um ferro de soldar e bibliotecas e plug-ins de terceiros.



Vários governantes

Fluxo TypeScript
Contribuidor principal Facebook Microsoft
Local na rede Internet flow.org www.typescriptlang.org
Github github.com/facebook/flow github.com/microsoft/TypeScript
GitHub é iniciado 21,3k 70,1k
GitHub Forks 1.8k 9,2 k
Problemas do GitHub: aberto / fechado 2,4k / 4,1k 4,9k / 25,0k
StackOverflow ativo 2289 146.221
StackOverflow Frequent 123 11451


Olhando para esses números, simplesmente não tenho o direito moral de recomendar o uso do fluxo. Mas por que eu mesmo usei? Porque costumava haver algo como tempo de execução de fluxo.



flow-runtime



flow-runtime é um conjunto de plug-ins para babel que permite embutir tipos de fluxo em tempo de execução, usá-los para definir tipos de variáveis ​​em tempo de execução e, o mais importante para mim, permite que você verifique os tipos de variáveis ​​em tempo de execução. Isso permitido em tempo de execução durante, por exemplo, autotestes ou testes manuais, para detectar bugs adicionais no aplicativo.



Ou seja, bem no tempo de execução (no assembly de depuração, é claro), o aplicativo verifica explicitamente todos os tipos de variáveis, argumentos, resultados de chamadas para funções de terceiros e tudo, tudo, tudo, para conformidade com esses tipos.



Infelizmente, para o novo ano de 2021, o autor do repositório adicionou informaçõesque ele não está mais envolvido no desenvolvimento deste projeto e, em geral, muda para o TypeScript. Na verdade, o último motivo para continuar no fluxo tornou-se obsoleto para mim. Bem, bem-vindo ao TypeScript.



All Articles