TypeScript vs. JavaScript: seu guia básico
Publicados: 2022-03-11TypeScript ou JavaScript? Os desenvolvedores contemplam essa escolha para projetos da Web ou Node.js greenfield, mas é uma questão que vale a pena considerar também para projetos existentes. Um superconjunto de JavaScript, o TypeScript oferece todos os recursos do JavaScript, além de algumas vantagens adicionais. O TypeScript nos encoraja intrinsecamente a codificar de forma limpa, tornando o código mais escalável. No entanto, os projetos podem conter tanto JavaScript simples quanto desejarmos, portanto, usar o TypeScript não é uma proposta de tudo ou nada.
A relação entre TypeScript e JavaScript
O TypeScript adiciona um sistema de tipos explícito ao JavaScript, permitindo a aplicação estrita de tipos de variáveis. O TypeScript executa suas verificações de tipo durante a transpilação — uma forma de compilação que converte o código TypeScript para o código JavaScript que os navegadores da Web e o Node.js entendem.
Exemplos de TypeScript vs. JavaScript
Vamos começar com um trecho de JavaScript válido:
let var1 = "Hello"; var1 = 10; console.log(var1); Aqui, var1 começa como uma string e depois se torna um number .
Como o JavaScript é apenas vagamente tipado, podemos redefinir var1 como uma variável de qualquer tipo – de uma string a uma função – a qualquer momento.
A execução deste código gera 10 .
Agora, vamos alterar este código para TypeScript:
let var1: string = "Hello"; var1 = 10; console.log(var1); Nesse caso, declaramos que var1 é uma string . Em seguida, tentamos atribuir um número a ele, o que não é permitido pelo sistema de tipos estrito do TypeScript. A transpilação resulta em um erro:
TSError: ⨯ Unable to compile TypeScript: src/snippet1.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'. 2 var1 = 10; Se instruíssemos o transpilador a tratar o trecho de JavaScript original como se fosse TypeScript, o transpilador inferiria automaticamente que var1 deveria ser uma string | number string | number . Este é um tipo de união TypeScript, que nos permite atribuir var1 uma string ou um number a qualquer momento. Tendo resolvido o conflito de tipo, nosso código TypeScript seria transpilado com sucesso. Executá-lo produziria o mesmo resultado que o exemplo JavaScript.
TypeScript vs. JavaScript a partir de 30.000 pés: desafios de escalabilidade
O JavaScript é onipresente, impulsionando projetos de todos os tamanhos, aplicados de maneiras inimagináveis durante sua infância na década de 1990. Embora o JavaScript tenha amadurecido, ele fica aquém quando se trata de suporte à escalabilidade. Assim, os desenvolvedores lidam com aplicativos JavaScript que cresceram em magnitude e complexidade.
Felizmente, o TypeScript aborda muitos dos problemas de dimensionamento de projetos JavaScript. Vamos nos concentrar nos três principais desafios: validação, refatoração e documentação.
Validação
Contamos com ambientes de desenvolvimento integrados (IDEs) para ajudar em tarefas como adicionar, modificar e testar novos códigos, mas os IDEs não podem validar referências JavaScript puras. Nós mitigamos essa falha monitorando atentamente enquanto codificamos para evitar a possibilidade de erros de digitação em variáveis e nomes de funções.
A magnitude do problema cresce exponencialmente quando o código se origina de um terceiro, onde referências quebradas em ramificações de código raramente executadas podem facilmente passar despercebidas.
Em contraste, com o TypeScript, podemos concentrar nossos esforços na codificação, confiantes de que quaisquer erros serão identificados no tempo de transpilação. Para demonstrar isso, vamos começar com algum código JavaScript legado:
const moment = require('moment'); const printCurrentTime = (format) => { if (format === 'ISO'){ console.log("Current ISO TS:", moment().toISO()); } else { console.log("Current TS: ", moment().format(format)); } } A chamada .toISO() é um erro de digitação do método moment.js toISOString() , mas o código funcionaria, desde que o argumento de format não fosse ISO . A primeira vez que tentarmos passar ISO para a função, ele irá gerar este erro de execução: TypeError: moment(...).toISO is not a function .
Localizar o código com erros ortográficos pode ser difícil. A base de código atual pode não ter um caminho para a linha quebrada, caso em que nossa referência .toISO() quebrada não seria capturada pelo teste.
Se portarmos esse código para o TypeScript, o IDE destacará a referência quebrada, solicitando que façamos correções. Se não fizermos nada e tentarmos transpilar, seremos bloqueados e o transpilador gerará o seguinte erro:
TSError: ⨯ Unable to compile TypeScript: src/catching-mistakes-at-compile-time.ts:5:49 - error TS2339: Property 'toISO' does not exist on type 'Moment'. 5 console.log("Current ISO TS:", moment().toISO());Reestruturação
Embora erros de digitação em referências de código de terceiros não sejam incomuns, há um conjunto diferente de problemas associados a erros de digitação em referências internas, como este:
const myPhoneFunction = (opts) => { // ... if (opts.phoneNumbr) doStuff(); } Um único desenvolvedor pode localizar e corrigir todas as instâncias de phoneNumbr para terminar com er facilidade.
Mas quanto maior a equipe, mais esse erro simples e comum é excessivamente caro. No decorrer de seu trabalho, os colegas precisariam estar cientes e propagar esses erros de digitação. Como alternativa, adicionar código para suportar ambas as grafias aumentaria a base de código desnecessariamente.
Com o TypeScript, quando corrigimos um erro de digitação, o código dependente não será mais transpilado, sinalizando aos colegas para propagar a correção em seu código.
Documentação
Documentação precisa e relevante é a chave para a comunicação dentro e entre as equipes de desenvolvedores. Os desenvolvedores de JavaScript geralmente usam JSDoc para documentar métodos e tipos de propriedades esperados.
Os recursos de linguagem do TypeScript (por exemplo, classes abstratas, interfaces e definições de tipo) facilitam a programação de projeto por contrato, levando a uma documentação de qualidade. Além disso, ter uma definição formal dos métodos e propriedades aos quais um objeto deve aderir ajuda a identificar alterações importantes, criar testes, realizar introspecção de código e implementar padrões de arquitetura.
Para TypeScript, a ferramenta principal TypeDoc (baseada na proposta TSDoc) extrai automaticamente informações de tipo (por exemplo, classe, interface, método e propriedade) do nosso código. Assim, criamos sem esforço uma documentação que é, de longe, mais abrangente que a do JSDoc.
Vantagens do TypeScript vs. JavaScript
Agora, vamos explorar como podemos usar o TypeScript para lidar com esses desafios de escalabilidade.
Sugestões avançadas de código/refatoração
Muitos IDEs podem processar informações do sistema de tipos TypeScript, fornecendo validação de referência à medida que codificamos. Melhor ainda, conforme digitamos, o IDE pode fornecer documentação relevante e rápida (por exemplo, os argumentos que uma função espera) para qualquer referência e sugerir nomes de variáveis contextualmente corretos.
Neste trecho do TypeScript, o IDE sugere um preenchimento automático dos nomes das chaves dentro do valor de retorno da função:
/** * Simple function to parse a CSV containing people info. * @param data A string containing a CSV with 3 fields: name, surname, age. */ const parsePeopleData = (data: string) => { const people: {name: string, surname: string, age: number}[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; const tokens = row.split(',').map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3){ errors.push(`Row "${row}" contains only ${tokens.length} tokens. 3 required`); continue; } people.push({ name: tokens[0], surname: tokens[1], age: +tokens[2] }) } return {people, errors}; }; const exampleData = ` Gordon,Freeman,27 G,Man,99 Alyx,Vance,24 Invalid Row,, Again, Invalid `; const result = parsePeopleData(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }Meu IDE, Visual Studio Code, forneceu esta sugestão (no texto explicativo) quando comecei a chamar a função (linha 31):
Além disso, as sugestões de preenchimento automático do IDE (no texto explicativo) estão contextualmente corretas, mostrando apenas nomes válidos em uma situação de chave aninhada (linha 34):
Essas sugestões em tempo real levam a uma codificação mais rápida. Além disso, os IDEs podem contar com as informações de tipo rigorosas do TypeScript para refatorar o código em qualquer escala. Operações como renomear uma propriedade, alterar localizações de arquivos ou até mesmo extrair uma superclasse tornam-se triviais quando estamos 100% confiantes na precisão de nossas referências.

Suporte de interface
Ao contrário do JavaScript, o TypeScript oferece a capacidade de definir tipos usando interfaces . Uma interface lista formalmente — mas não implementa — os métodos e propriedades que um objeto deve incluir. Essa construção de linguagem é particularmente útil para colaboração com outros desenvolvedores.
O exemplo a seguir destaca como podemos aproveitar os recursos do TypeScript para implementar padrões OOP comuns de forma organizada - neste caso, estratégia e cadeia de responsabilidade - melhorando assim o exemplo anterior:
export class PersonInfo { constructor( public name: string, public surname: string, public age: number ){} } export interface ParserStrategy{ /** * Parse a line if able. * @returns The parsed line or null if the format is not recognized. */ (line: string): PersonInfo | null; } export class PersonInfoParser{ public strategies: ParserStrategy[] = []; parse(data: string){ const people: PersonInfo[] = []; const errors: string[] = []; for (let row of data.split('\n')){ if (row.trim() === '') continue; let parsed; for (let s of this.strategies){ parsed = s(row); if (parsed) break; } if (!parsed){ errors.push(`Unable to find a strategy capable of parsing "${row}"`); } else { people.push(parsed); } } return {people, errors}; } } const exampleData = ` Gordon,Freeman,27 G;Man;99 {"name":"Alyx", "surname":"Vance", "age":24} Invalid Row,, Again, Invalid `; const parser = new PersonInfoParser(); const createCSVStrategy = (fieldSeparator = ','): ParserStrategy => (line) => { const tokens = line.split(fieldSeparator).map(i => i.trim()).filter(i => i != ''); if (tokens.length < 3) return null; return new PersonInfo(tokens[0], tokens[1], +tokens[2]); }; parser.strategies.push( (line) => { try { const {name, surname, age} = JSON.parse(line); return new PersonInfo(name, surname, age); } catch(err){ return null; } }, createCSVStrategy(), createCSVStrategy(';') ); const result = parser.parse(exampleData); console.log("Parsed People:"); console.log(result.people. map(p => `Name: ${p.name}\nSurname: ${p.surname}\nAge: ${p.age}`) .join('\n\n') ); if (result.errors.length > 0){ console.log("\nErrors:"); console.log(result.errors.join('\n')); }Módulos ES6—Em qualquer lugar
Até o momento, nem todos os tempos de execução JavaScript front-end e back-end suportam módulos ES6. Com o TypeScript, no entanto, podemos usar a sintaxe do módulo ES6:
import * as _ from 'lodash'; export const exampleFn = () => console.log(_.reverse(['a', 'b', 'c'])); A saída transpilada será compatível com nosso ambiente selecionado. Por exemplo, usando a opção do compilador --module CommonJS , obtemos:
"use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; Usando --module UMD em vez disso, o TypeScript gera o padrão UMD mais detalhado:
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports", "lodash"], factory); } })(function (require, exports) { "use strict"; exports.__esModule = true; exports.exampleFn = void 0; var _ = require("lodash"); var exampleFn = function () { return console.log(_.reverse(['a', 'b', 'c'])); }; exports.exampleFn = exampleFn; });Aulas ES6 — Em qualquer lugar
Ambientes legados geralmente não têm suporte para classes ES6. Uma transpilação do TypeScript garante a compatibilidade usando construções específicas de destino. Aqui está um trecho de fonte do TypeScript:
export class TestClass { hello = 'World'; }A saída JavaScript depende do módulo e do destino, que o TypeScript nos permite especificar.
Aqui está o que --module CommonJS --target es3 produz:
"use strict"; exports.__esModule = true; exports.TestClass = void 0; var TestClass = /** @class */ (function () { function TestClass() { this.hello = 'World'; } return TestClass; }()); exports.TestClass = TestClass; Usando --module CommonJS --target es6 em vez disso, obtemos o seguinte resultado transpilado. A palavra-chave class é usada para direcionar o ES6:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestClass = void 0; class TestClass { constructor() { this.hello = 'World'; } } exports.TestClass = TestClass;Funcionalidade assíncrona/aguardar — em qualquer lugar
Async/await torna o código JavaScript assíncrono mais fácil de entender e manter. O TypeScript oferece essa funcionalidade para todos os runtimes, mesmo para aqueles que não fornecem async/await nativamente.
Observe que para executar async/await em tempos de execução mais antigos, como ES3 e ES5, você precisará de suporte externo para saída baseada em Promise (por exemplo, via Bluebird ou um polyfill ES2015). O polyfill Promise que vem com o TypeScript integra-se facilmente à saída transpilada — só precisamos configurar a opção do compilador lib de acordo.
Suporte para campos de classe privada — em qualquer lugar
Mesmo para destinos legados, o TypeScript oferece suporte a campos private da mesma maneira que linguagens fortemente tipadas (por exemplo, Java ou C#). Em contraste, muitos runtimes JavaScript suportam campos private por meio da sintaxe de prefixo de hash, que é uma proposta finalizada do ES2022.
Desvantagens do TypeScript vs. JavaScript
Agora que destacamos os principais benefícios da implementação do TypeScript, vamos explorar os cenários em que o TypeScript pode não ser a opção certa.
Transpilação: potencial para incompatibilidade de fluxo de trabalho
Fluxos de trabalho específicos ou requisitos de projeto podem ser incompatíveis com a etapa de transpilação do TypeScript: por exemplo, se precisarmos usar uma ferramenta externa para alterar o código após a implantação ou se a saída gerada precisar ser amigável ao desenvolvedor.
Por exemplo, recentemente escrevi uma função do AWS Lambda para um ambiente Node.js. O TypeScript não foi adequado porque exigir transpilação impediria que eu e outros membros da equipe editassem a função usando o editor online da AWS. Este foi um disjuntor do negócio para o gerente de projeto.
Tipo de sistema funciona apenas até o tempo de transpile
A saída JavaScript do TypeScript não contém informações de tipo, portanto, não executará verificações de tipo e, portanto, a segurança de tipo pode ser interrompida em tempo de execução. Por exemplo, suponha que uma função seja definida para sempre retornar um objeto. Se null for retornado de seu uso em um arquivo .js , ocorrerá um erro de tempo de execução.
Recursos dependentes de informações de tipo (por exemplo, campos privados, interfaces ou genéricos) agregam valor a qualquer projeto, mas são eliminados durante a transpilação. Por exemplo, os membros da classe private não seriam mais privados após a transpilação. Para ser claro, problemas de tempo de execução dessa natureza não são exclusivos do TypeScript, e você também pode encontrar as mesmas dificuldades com JavaScript.
Combinando TypeScript e JavaScript
Apesar dos muitos benefícios do TypeScript, às vezes não podemos justificar a conversão de um projeto JavaScript inteiro de uma só vez. Felizmente, podemos especificar para o transpilador TypeScript — arquivo por arquivo — o que interpretar como JavaScript simples. Na verdade, essa abordagem híbrida pode ajudar a mitigar os desafios individuais à medida que eles surgem ao longo do ciclo de vida de um projeto.
Podemos preferir deixar o JavaScript inalterado se o código:
- Foi escrito por um ex-colega e exigiria esforços significativos de engenharia reversa para converter para TypeScript.
- Usa técnica(s) não permitida(s) no TypeScript (por exemplo, adiciona uma propriedade após a instanciação do objeto) e exigiria refatoração para aderir às regras do TypeScript.
- Pertence a outra equipe que continua a usar JavaScript.
Nesses casos, um arquivo de declaração (arquivo .d.ts , às vezes chamado de arquivo de definição ou arquivo de tipagem) fornece ao TypeScript dados de tipo suficientes para permitir sugestões de IDE enquanto deixa o código JavaScript como está.
Muitas bibliotecas JavaScript (por exemplo, Lodash, Jest e React) fornecem arquivos de tipagem TypeScript em pacotes de tipos separados, enquanto outras (por exemplo, Moment.js, Axios e Luxon) integram arquivos de tipagem no pacote principal.
TypeScript vs. JavaScript: uma questão de racionalização e escalabilidade
O suporte, a flexibilidade e os aprimoramentos incomparáveis disponíveis por meio do TypeScript melhoram significativamente a experiência do desenvolvedor, permitindo que projetos e equipes sejam dimensionados. O principal custo de incorporar o TypeScript em um projeto é a adição da etapa de compilação da transpilação. Para a maioria dos aplicativos, transpilar para JavaScript não é um problema; em vez disso, é um trampolim para os muitos benefícios do TypeScript.
Leitura adicional no Blog da Toptal Engineering:
- Trabalhando com suporte a TypeScript e Jest: um tutorial do AWS SAM
