Looplex Knowledge Base

TypeScript, vale a pena?

TypeScript, vale a pena?

Banner TypeScript

Vez ou outra, o destino nos permite criar um projeto greenfield. Nestas horas, o impulso inicial é pularmos no stack da moda, utilizarmos the latest and greatest. Por exemplo, agora em 2020 para front-end seria mais ou menos assim: JAMStack? Sim! Next.js? Sim! React? Sim! TypeScript? Sim! E, provavelmente, são nesses momentos de euforia que mais cometemos erros nas nossas vidas.

"A experiência é o nome que damos aos nossos erros."
— Oscar Wilde.

A idéia desse post é compartilhar um pouco da minha "experiência" com o universo frontend, mas com foco na linguagem TypeScript. Colocar minhas dúvidas e argumentos prós/contra em relação à adoção da linguagem em nossa stack. Com isso, contribuir nas decisões coletivas que estão por vir.

Vou começar com um breve relato histórico sobre a evolução do Javascript a partir de 2009, momento em que acredito ter começado o movimento de diversas comunidades mirando no Javascript como target de compilação.

Depois vou utilizar a documentação da versão inicial do projeto para mostrar os problemas que a linguagem se propunha a resolver, também farei uso de alguns backups para criar e sustentar meus argumentos.

A parte final é reservada para conclusão que será feita conjuntamente em cima de temas que deixarei pré-definidos.

Um pouco de história

Parece ontem, mas depois de 10 anos, em 2009, finalmente saia a atualização do ECMA-262, a tão aguardada ES5. A nova versão da linguagem padronizava funcionalidades já consagradas em bibliotecas da comunidade como uso do JSON, métodos funcionais em arrays e também pequenas features que não podiam entrar na linguagem através de monkey patching. Segue abaixo a relação completa de modificações:

  • String.trim()
  • Array.isArray()
  • Array.forEach()
  • Array.map()
  • Array.filter()
  • Array.reduce()
  • Array.reduceRight()
  • Array.every()
  • Array.some()
  • Array.indexOf()
  • Array.lastIndexOf()
  • JSON.parse()
  • JSON.stringify()
  • Date.now()
  • Property Getters and Setters
  • Object.defineProperty(object, property, descriptor)
  • Object.defineProperties(object, descriptors)
  • Object.getOwnPropertyDescriptor(object, property)
  • Object.getOwnPropertyNames(object)
  • Object.keys(object)
  • Object.getPrototypeOf(object)
  • Object.preventExtensions(object)
  • Object.isExtensible(object)
  • Object.seal(object)
  • Object.isSealed(object)
  • Object.freeze(object)
  • Object.isFrozen(object)
  • "use strict"
  • Acesso ao character de uma string através da notação [ ], como no C.
  • Permitir , adicional ao final do último elemento de arrays e objetos.
  • Permitir que strings ocupem múltiplas linhas com o character de escape \ no final da linha.
  • Permitir uso de reserved words como nomes de propriedades em objetos.

Como podemos constatar, o update era pequeno e trazia pouca inovação sobre o que existia anteriormente através de "extensões da linguagem" como Prototype e Mootools.

Coincidentemente, neste mesmo ano, Zach Carter (@zaach) disponibilizava o Jison, a versão Javascript do GNU Bison de Robert Corbett e Richard Stallman (1985).

Para quem não conhece, o GNU Bison é um general-purpose parser generator que recebe uma gramática livre de contexto e converte em um parser utilizando LALR(1) parser tables. Para leigos, podemos dizer que Bison e Jison são linguagens para gerar linguagens de programação. Linguagens de programação possuem um (1) compilador e, opcionalmente, um (2) interpretador cujas funções são estas:

  1. O compilador é a ferramenta que recebe o programa em uma linguagem e transforma em outra.
    e.g. Os compiladores antigos transformavam programas escritos em A-0, Fortran e Cobol em linguagem de máquina, mas hoje em dia é comum transformar programas de uma linguagem para a outra, como é o caso do TypeScript para Javascript.
  2. O interpretador é a ferramenta que recebe um programa e o executa.
    e.g. Antigamente, o CPU era o único interpretador, mas com o avanço computacional e modernas abstrações, temos hoje em dia as virtual machines, como é o caso do V8 que interpreta Javascript e WebAssembly nos navegadores derivados do projeto Chromium e também no servidor com Node.js.

Veja abaixo um overview da anatomia de uma linguagem de programação:

Anatomia de uma linguagem

Compilando para Javascript

Diante da lentidão do ECMA em atualizar a linguagem, da grande quantidade de features que podiam ser incluídos na linguagem através de monkey patching e o lançamento do Jison, a comunidade buscou formas alternativas de melhorar a usabilidade para sua base de usuários. Esse foi o pontapé inicial para uma nova fase da comunidade Javascript, pois agora existia a ferramenta para pegar o conhecimento criado desde 1985 com Flex/Bison para criar rapidamente linguagens que rodam sobre o Javascript. Não era mais necessário escrever compiladores manualmente, não precisaria nem usar a syntax do Javascript.

Tivemos uma primavera de linguagens do tipo x-to-Javascript, movimento que fazia total sentido após várias palestras de Douglas Crockford —criador do JSON— em 2007 que culminaram na publicação do livro "Javascript: The Good Parts" (2008) cuja narrativa é, em linhas gerais: "Toda linguagem de programação tem partes boas e ruins. Cabe ao programador utilizar apenas o subset bom, que é esse do meu livro...".

Meme comparando a espessura do Javascript, The Definitive Guide com o Javascript, The Good Parts

No final de 2009, Jeremy Ashkenas lançou a primeira versão do CoffeeScript, uma linguagem feita com Jison e forte influência do Ruby na syntax que tentava "expor as partes boas do Javascript de uma forma simples".

CoffeeScript is a little language that compiles into Javascript. Underneath that awkward Java-esque patina, Javascript has always had a gorgeous heart. CoffeeScript is an attempt to expose the good parts of Javascript in a simple way.

Em 2010, dificilmente existia evento onde não se falasse de CoffeeScript. O sucesso dessa iniciativa no ecossistema Ruby e Javascript mostrarava que a estratégia de enriquecer uma comunidade não fluente em Javascript a migrar para o front utilizando uma sintaxe familiar era viável. Então, gigantes da tecnologia como Google e Microsoft começaram a trabalhar em versões para aplicar em seus feudos e, quem sabe, aumentar o seu marketshare nessa nova área.

Duas estratégias diferentes

Google

O leak de um email com o resultado de um summit interno no Google em 2010, deixava clara a insatisfação com o estado da web como plataforma e o medo de tecnologias emergentes como o iOS:

Complex web apps--the kind that Google specializes in--are struggling against the platform and working with a language that cannot be tooled and has inherent performance problems. Even smaller-scale apps written by hobbyist developers have to navigate a confusing labyrinth of frameworks and incompatible design patterns.

The web has succeeded historically to some extent in spite of the web platform, based primarily on the strength of its reach. The emergence of compelling alternative platforms like iOS has meant that the web platform must compete on its merits, not just its reach. Javascript as it exists today will likely not be a viable solution long-term.

Foram criados dois produtos como resultado desse exercício: O Traceur, um compilador ESNext-ES5 para a estratégia "evolve Javascript" e o Dash/Dart, uma nova linguagem de programação cujo objetivo era se tornar a lingua franca de desenvolvimento web para a estratégia "clean break".

Para este artigo, vamos focar nas key features que eles acreditavam estar consertando com o Dash/Dart:

  • Classes — "Classes and interfaces provide a well understood mechanism for efficiently defining APIs. These constructs enable encapsulation and reuse of methods and data."
  • Tipagem opcional — "Dart programmers can optionally add static types to their code. Depending on programmer preference and stage of application development, the code can migrate from a simple, untyped experimental prototype to a complex, modular application with typing. Because types state programmer intent, less documentation is required to explain what is happening in the code, and type-checking tools can be used for debugging."
  • Ferramental — "Dart will include a rich set of execution environments, libraries, and development tools built to support the language. These tools will enable productive and dynamic development, including edit-and-continue debugging and beyond—up to a style where you program an application outline, run it, and fill in the blanks as you run."
  • Bibliotecas — "Developers can create and use libraries that are guaranteed not to change during runtime. Independently developed pieces of code can therefore rely on shared libraries."

Microsoft

Diferentemente da Google, o time da Microsoft apostava na melhoria contínua da linguagem com o comitê:

We strongly believe in the community process driven through TC39 as the keepers of these principles. TC39’s work on ECMAScript 5 is a solid step forward for JavaScript in the Web platform. Some of the principles used to design ES5 provide a good template for future versions of the JavaScript standard:

  • Preserving the fundamental syntax of tomorrow’s “text/javascript” to ensure continuity in the developer skillset and deliver seamless compatibility between today’s JavaScript and tomorrow’s JavaScript.
  • Provide features that are additive and pay-as-you-go, to help developers get more value with minimal new effort or learning.
  • Strive for features to be locally detectable, to help developers build applications that work on the broadest range of browsers.
  • Where possible, allow for a possibly slower library alternative (or ‘polyfill’) for browsers that do not yet support the feature.

We are working with TC39 on applying these same approaches as broadly as possible for the next revision of the ECMAScript standard.

Some examples [of compile-to-Javascript frameworks], like Dart, portend that Javascript has fundamental flaws and to support these scenarios requires a "clean break" from Javascript in both syntax and runtime. We disagree with this point of view. We believe that with committee participant focus, the standards runtime can be expanded and the syntatic features necessary to support Javascript at scale can be built upon existing Javascript standard.

Ou seja, a Microsoft apostou as fichas na "estratégia evolutiva". No seu lançamento, o TypeScript criado por Anders Hejlsberg, chegava ao mercado com os seguintes diferenciais:

  • Escalável — "O TypeScript oferece classes, módulos, e interfaces para ajudar você a construir componentes robustos. Essas funcionalidades estão disponíveis durante o desenvolvimento para uma experiência de alta confiança na construção de aplicativos, mas são compiladas para Javascript simples. Os tipos em TypeScript permitem que você defina interfaces entre os componentes para ganhar insights dos comportamentos de bibliotecas existentes."
  • Ferramental — "Os tipos permitem que os desenvolvedores de TypeScript utilizem práticas e ferramentas de alta produtividade: checagem estática, navegação por símbolos, complemento de declaração, e refatoração de código. Os tipos são opcionais, e a inferência de tipos permite que algumas poucas anotações façam uma grande diferença para a verificação estática do código."
  • Compatibilidade "O TypeScript baseia-se na sintaxe e semântica que milhões de desenvolvedores Javascript já sabem. Com TypeScript, você pode utilizar códigos Javascript atuais, incorporar bibliotecas populares, e ser consumido por código Javascript. TypeScript é compilado para Javascript simples e limpo que roda em qualquer browser, node.js, ou qualquer ambiente compatível com ES3 (1999)."

Sucesso do TypeScript

Notemos que ambos os projetos propõe soluções para os mesmos problemas fundamentais:

  1. A herança prototipada é desconhecida pela grande maioria dos programadores, é muito difícil migrar talentos de outras plataformas para colaborar em grandes projetos.
  2. A ausência da tipagem no código fonte não fornece informação suficiente que as IDEs facilitem a vida do programador. Eles estão usando vim ou emacs com plugins tipo jslint.

Revisitando esses temas, me parece que a única diferença filosófica entre o Dart e o TypeScript é em relação à compatibilidade que teriam com o ecossistema Javascript. Enquanto o Dart buscava uma quebra total de compatibilidade total, o TypeScript buscou a conciliação com a comunidade.

Enquanto o Dart oferecia um runtime robusto, interpretando exatamente o que o desenvolvedor programou; o TypeScript transformaria tudo em Javascript e utilizaria o ecossistema presente.

Hoje em dia, sabemos qual é o produto vencedor nesse espaço de linguagens compiladas para Javascript:

CoffeeScript, Dart and TypeScript in a chart from 2009 to 2020

É muito interessante, e fato recorrente na história, observar que a melhor solução técnica —Dart—, mesmo com sua melhor performance e checagem real de tipos em runtime dentro da sua VM, perdeu o mercado para o TypeScript. Qual foi o motivo?

A minha teoria é a seguinte:

Do gráfico acima, podemos observar que nenhum desses compiladores gerou tração fora comunidade onde nasceram: Os rubyistas usavam CoffeScript, quem trabalhava com o Google SDK usava Dart e quem trabalhava com Visual Studio se interessava por TypeScript. Então, o que aconteceu por volta de março de 2016 que o TypeScript cresceu de forma tão expressiva?

Para mim, Satya Nadella aconteceu. O novo CEO da Microsoft, indicado por Bill Gates no início de 2014, revolucionou a empresa; especialmente no relacionamento com os desenvolvedores do mundo open source. O Visual Studio Code, um produto lançado em Novembro de 2015 pela Microsoft, era:

  • Open Source
  • Cross Platform
  • Rápido (quando comparado com os concorrentes da época)
  • Trazia "pré-configurado" toda experiência de usuário do Visual Studio gratuitamente. Estavam incluídos IntelliSense, Debugging tools, Git Integration e um incrível sistema de plugins sob um stack que todo desenvolvedor web podia colaborar.

O crescimento da base de desenvolvedores usando o Visual Studio Code alavancou o interesse pelo TypeScript que possuia integração premium com o editor, oferecendo features que os desenvolvedores frontend simplesmente não estavam acostumados.

Visual Studio Code, Adobe Brackets, Github Atom, Sublime Text and Vim chart from Nov 2015 to 2020

Então acredito que foi o lançamento do Visual Studio Code, sua integração excelente com TypeScript e especialmente com várias outras comunidades através de plugins que fez o TypeScript ser reconhecido como opção fora do universo Microsoft.

Ótimo, então o TypeScript é o vencedor e devemos utilizá-lo, certo? Talvez, mas grande parte do trabalho intelectual é procurar a verdade mesmo quando estas contrariam suas convicções. Lembram que o Dart era a melhor tecnologia? Na minha humilde opinião, o TypeScript possui características que despertam alertas.

Desvantagens

TypeScript não é um superset do Javascript

Depois de falar tanto de compilar para Javascript, é fácil esquecermos que existe um mundo onde o Javascript existe sozinho. O Javascript atual, é muito diferente do que existia em 2009, ou 2012 quando o ES5 começou a ser utilizado em produção. Vivemos em um período pós ES2015, ES2016, ES2017, ES2018, ES2019, ES2020 et cetera.

O que vejo atualmente é que o TypeScript começou a correr para implementar features que a comunidade usa, mas não funcionam bem com a linguagem ou que já existem no standard, mas não no TypeScript. Veja por exemplo um release do TypeScript 4.0: Short-Circuiting Assignment Operators. Se você abrir o browser e escrever no console:

let a = false
let b = true
a ||= b

A chance de você receber true, é gigante, mesmo o equivalente Typescript —anterior à 4.0— ser um erro:

let a: boolean = false;
let b: boolean = true;
a ||= b;
----^ Expression expected.

Ou seja, não existe mais a vantagem inicial de todo Javascript ser compatível com TypeScript. Não se pode pegar um arquivo .js e renomear para .ts e ter a garantia de que tudo continuará funcionando. Esse statement era possível em 2012 porque a linguagem Javascript praticamente não evoluia, mas a situação atual é bem diferente, o standard é atualizado anualmente, o que faz necessário acompanhar 2 projetos ao invés de 1.

Escalabilidade não é mais um problema

Outra grande bandeira que atualmente perdeu o sentido, é em relação à arquitetura que permite escalabilidade. Como descrito anteriormente, um dos grandes diferenciais dessas novas linguagens era a possibilidade de escrever classes e módulos de forma tradicional, como era feito em Java e C#:

public class MyClass {
int x = 42;
public static void main(String[] args) {
MyClass myObj = new MyClass();
System.out.println(myObj.x);
}
}

Em ES3, essa classe poderia ser escrita de diversas formas, mas a canônica seria assim:

function MyClass() {
this.x = 42;
}
MyClass.prototype.main = function() {
var args = Array.prototype.slice.call(arguments);
var myObj = new MyClass();
console.log(myObj.x);
}

Mas depois do ES2015, a construção abaixo também válida, diminuindo a curva de aprendizado para programadores de Java e C#:

class MyClass {
constructor() {
this.x = 42;
}
main(...args) {
const myObj = new MyClass();
console.log(myObj.x);
}
}

Analogamente, antes do ES2015, não existia forma canônica de criar e importar módulos, mas este problema também já foi corrigido:

// lib/foobar.js
export default {
foobar: "foobar"
}
// main.js
import { foobar } from "./lib/foobar.js"
console.log(foobar)

Orientação à Objetos pura é restritiva

Uma das mais celebradas adições ao ES2015 foi a notação de classes e módulos. A comunidade React pulou com força nesse barco. De 2015 até 2019, os desenvolvedores sentiram a miséria que era trabalhar nesse modelo, mas hoje em dia o padrão é mais funcional, no sentido de pure-functions como as funções matemáticas, pois elas oferecem testabilidade, composibilidade e paralelismo criando sistemas naturalmente distribuídos, desacoplados e side-effects free.

Neste ponto, o problema do TypeScript é ser menos multiparadigma que Javascript, ele tem tendências para facilitar a sintaxe orientada à objetos. Atualmente, é reconhecido que o paradigma imperativo orientado à objetos não resolve todos os problemas, especialmente quando se usa os estados privados do objeto que impossibilitam, por exemplo, ferramentas como time machine.

Vale a pena relembrar das objeções do criador do Erlang, Joe Armstrong, ao modelo de programação orientada a objetos e a famosa equação do criador da linguagem Scala, Martin Odersky:

non-determinism = parallel processing + mutable state

O problema disso, é que com o shift da comunidade para o paradigma funcional, trabalhar com TypeScript dificulta em muitos casos, apesar do suporte estar melhorando com updates da linguagem. Por exemplo: dificuldades em tipar corretamente High-Order Components se mostra um desafio recorrente na comunidade, pois uma função, como first-class citizen, não possui nenhum tipo.

Por exemplo, desde de 2019, a comunidade React está experimentando trabalhar com Reason, uma linguagem derivada do OCaml, através do ReasonReact.

Reason lets you write simple, fast and quality type safe code while leveraging both the JavaScript & OCaml ecosystems.

ReasonReact helps you use Reason to build React components with deeply > integrated, strong, static type safety.

It is designed and built by people using Reason and React in large, mission > critical production React codebases.

ReasonReact uses Reason's expressive language features, along with smooth > JavaScript interop to provide a React API that is:

  • Safe and statically typed (with full type inference).
  • Simple and lean.
  • Familiar and easy to insert into an existing ReactJS codebase.

Aqui tem um vídeo do Jordan Walke, autor do React, falando sobre o futuro da plataforma:

TypeScript é muito mais pesado

O custo de carregar 50% de pacotes extras como developer dependencies no projeto é um ponto negativo:

project size comparison between javascript and typescript

Hot Module Replacement

O fato do TypeScript exigir uma etapa intermediária, de compilação, lentifica a experiência em módulos como Hot Module Replacement (HMR), que deveriam trazer feedback instantâneo durante o desenvolvimento:

showing that hot module replacement keeps compiling...

Escassez de mão de obra qualificada

Encontrar programadores Javascript é uma tarefa difícil. Encontrar programadores TypeScript é mais mais difícil.

TypeScript types são removidos em produção

O fato do Javascript adotar um structural type system e não um nominal reified type system, faz com que não faça sentido definirmos interfaces na linguagem. Essa é uma enorme diferença que passa despercebida aos programadores Java e C#. Em TypeScript, você pode escrever:

class Car {
drive() {
// hit the gas
}
}
class Golfer {
drive() {
// hit the ball far
}
}
let x: Car = new Golfer();

E não receber nenhum erro, quando claramente existe um problema de tipagem!

Outro problema: tanto em Java como C#, é normal pensar na correspondência dos tipos existindo em runtime e tipos declarados em compile-time. O fato do Javascript não possuir informações sobre os tipos TypeScript acaba gerando problemas, por exemplo:

Cria-se uma API em TypeScript utilizando interfaces, executa-se testes no Jest com TypeScript, tudo passa o dev faz commit e o sistema de CI/CD colocao sistema no ar para receber chamadas. Mas, uma vez online, acaba processando um monte de groselha desestruturada, pois os payloads não são validados. Portanto, o TypeScript não poupa o trabalho de definirmos validadores em runtime, tarefa que é realizada por tantas bibliotecas da comunidade e.g. jsonschema, jsv exigindo double work.

Outra peculiaridade do TypeScript, também relacionada à tipos, é que eles possuem comportamento de conjunto e não uma identidade declarada. Por exemplo:

let typeConfused: string|number|boolean;
typeConfused = "I'm a string!";
console.log(typeConfused);
typeConfused = 1;
console.log(typeConfused);
typeConfused = true;
console.log(typeConfused);

A variável typeConfused é confusa em relação ao seu tipo. Isso nos leva ao ponto interessante a seguir!

Vantagens

Vantagens no ferramental

Para resolver a ambiguidade exemplificada acima, o Visual Studio Code implementa, além do consumo de definição de tipos, uma análise para inferência de tipos e o bom é que essa informação funciona também para Javascript, sem TypeScript. Significa que grande parte dos casos de melhoria na UX do programador está coberta pelo mesmo mecanismo do TypeScript.

Type Inference working on Javascript

Facilita o Onboarding

Apesar do profissional de TypeScript ser mais difícil de encontrar, as TypeScript Declarations reduzem o atrito estático para um novo dev começar no projeto, pois a IDE vai poupá-lo de conhecer toda documentação — dá para começar sendo um programador CTRL+SPACE.

Conclusão

Apesar do TypeScript, aparentemente, ter vencido a batalha das linguagens compiladas para Javascript, a realidade é que a grande maioria da comunidade simplesmente não se importa ou prefere o uso de Javascript:

Chart showing that Javascript still owns the Web

O gráfico mostra que, pelo menos em um futuro próximo, não existirão arquivos .d.ts (definição dos tipos TypeScript) para todas bibliotecas que existem na comunidade Javascript, mesmo com o enorme esforço da Microsoft ajudando o projeto Definitely Typed.

Isso é ruim porque ao invés de melhorar a UX do programador, ela piora com os erros aparecendo no editor. Quando as definições são escritas por outros devs que não os autores do módulo, as elas sofrem atraso para serem atualizadas deixando o IntelliSense desatualizado.

O fato de grande parte das novidades do TypeScript, da 3.0 em diante, mostrarem que atualmente a linguagem está na cola do Javascript para implementar features como objects spreads on generic types, enabling tons of existing JavaScript patterns on functions ou mesmo o casos de features novas como optional chaining and nullish coalescing que chegou no TypeScript 3.7 em Nov 2019 e que funciona, sem intermediário, pois está no ES2020, parece mostrar que o objetivo inicial do projeto, que era melhorar a linguagem Javascript, foi alcançado e agora com releases anuais o Javascript avança tão rápido quanto o TypeScript. Parece que os grandes consumidores —Google, Mozilla, Microsoft—, do ECMA-262 estão todos trabalhando de perto no TC39 e logo que a feature entra para o standard é liberada para os usuários. É sério, abra o console e digite:

let customer = {
name: "Carl",
details: { age: 82 }
};
const customerCity = customer?.city ?? "Unknown city";
console.log(customerCity); // Unknown city

Portanto, mesmo a melhoria do ferramental precisa ser ponderada: enquanto as Typescript Declarations ajudam no IntelliSense, o programador Javascript continua com 2/3 dos benefícios —Type Inference e Based on JSDocs—, a experiência com o HMR é um degradada devido à lentidão nos feedbacks visuais e o custo do projeto aumenta tanto em espaço em disco como em complexidade para configurar o ambiente.

Edit this page on GitHub