Categorias

Como não aprender TypeScript

TypeScript é uma linguagem de programação de código aberto criada pela Microsoft, em outubro de 2012. Prestes a completar dez anos de existência, ela está mais popular do que nunca. De acordo com um levantamento realizado pelo Stack Overflow no ano passado, TypeScript apareceu na oitava posição entre as linguagens mais lembradas pelos entrevistados, surgindo espontaneamente em 30% das interações com 83 mil desenvolvedores. O índice PYPL de pesquisas realizadas no Google confirma a ascensão da linguagem, que também ocupa a oitava posição no ranking.

Ou seja, se você ainda não está trabalhando com TypeScript, você provavelmente irá trabalhar um dia. A grande pergunta é por onde começar a estudar a linguagem. Inicialmente, havia uma certa resistência da comunidade de desenvolvimento em relação ao TypeScript por se acreditar que a linguagem estava sendo criada para substituir o JavaScript. Essa era uma das muitas concepções erradas em torno do TypeScript.

Stefan Baumgartner

Stefan Baumgartner é um desenvolvedor web austríaco que escreve, palestra e organiza eventos sobre desenvolvimento de software e tecnologias web. Em um artigo publicado na internet, ele explora as frustrações de se tentar aprender TypeScript de um jeito divertido e ensina como contornar os obstáculos, evitando erros comuns no aprendizado e finalmente amar o TypeScript.

Com sua autorização, traduzimos e reproduzimos o artigo na íntegra:

“‘TypeScript e eu nunca seremos amigos’. Nossa, quantas vezes eu ouvi essa frase? Aprender TypeScript, mesmo em 2022, pode parecer frustrante. E por tantos motivos diferentes. Pessoas que escrevem Java ou C# e descobrem que as coisas estão funcionando de forma diferente do que deveriam. Pessoas que usaram JavaScript a maior parte do tempo e estão sendo criticadas por um compilador. Aqui estão alguns erros que vi pessoas cometerem ao começar a usar o TypeScript. Espero que sejam úteis para você!

Este artigo foi muito influenciado por Como não aprender Rust de Denys, que eu recomendo.

Erro 1: Ignorar JavaScript

TypeScript é um superconjunto de JavaScript e tem sido anunciado assim desde então. O que isso significa é que o JavaScript faz parte da linguagem. Todo ele. Escolher o TypeScript não lhe dá um cartão livre para abandonar o JavaScript e seu comportamento errático. Mas o TypeScript torna mais fácil entendê-lo. E você pode ver o JavaScript avançando em todos os lugares.

Veja minha postagem no blog sobre tratamento de erros, por exemplo. Seria muito razoável permitir a captura de um erro como você está acostumado em outras linguagens de programação:

try {
  // algo com Axios, por exemplo
} catch(e: AxiosError) {
//         ^^^^^^^^^^ Error 1196 💥
}

Mas isso não é possível. E o motivo é por causa de como os erros de JavaScript funcionam (verifique o respectivo artigo para mais detalhes). Código que faria sentido em TypeScript, mas não é factível em JavaScript.

Outro exemplo, usar Object.keys e esperar acesso de propriedade simples também é algo que você esperaria, mas causará problemas.

type Person = {
  name: string, age: number, id: number,
}
declare const me: Person;

Object.keys(me).forEach(key => {
  // 💥 a próxima linha lança rabisquinhos vermelhos em nós
  console.log(me[key])
})

Existe uma maneira de corrigir esse comportamento conforme detalhado aqui, mas essa correção não pode ser aplicada a todos os cenários. O TypeScript simplesmente não pode garantir com base em seu código que os tipos de acesso a essa propriedade serão os esperados. Código que funciona perfeitamente bem apenas em JavaScript, mas que é difícil de expressar com o sistema de tipos por vários motivos.

Se você está aprendendo TypeScript sem nenhum histórico de JavaScript, comece a aprender a diferenciar entre JavaScript e o sistema de tipos. Além disso, aprenda a procurar as coisas certas. Parâmetros nomeados em funções. Você pode fazer isso com objetos como argumentos. Um belo padrão. É parte do JavaScript, no entanto. Encadeamento condicional? Implementado primeiro no compilador TypeScript, mas também é um recurso JavaScript. Classes e extensão de classes existentes? JavaScript. Campos de classe privada? Você sabe, aqueles com o # na frente deles, uma pequena cerca para que ninguém possa acessar o que está por trás. Também JavaScript.

O código do programa que realmente faz algo está na maioria das vezes no campo do JavaScript. Se você estiver usando tipos para expressar intenções e contratos, você está no reino dos tipos.

Recentemente, o site TypeScript tem uma declaração muito mais clara sobre o que significa usar TypeScript: TypeScript é JavaScript com sintaxe para tipos. Está bem aqui. TypeScript é JavaScript. Entender o JavaScript é a chave para entender o TypeScript.

Erro 2: Anotar tudo

Uma anotação de tipo é uma maneira de dizer explicitamente quais tipos esperar. Você sabe, as coisas que eram muito proeminentes em outras linguagens de programação, onde a verbosidade de StringBuilder stringBuilder = new StringBuilder() garante que você está realmente lidando com um StringBuilder. O oposto é a inferência de tipos, onde o TypeScript tenta descobrir o tipo para você. let a_number = 2 é do tipo number.

As anotações de tipo também são a diferença de sintaxe mais óbvia e visível entre TypeScript e JavaScript.

Ao começar a aprender TypeScript, você pode querer anotar tudo para expressar os tipos esperados. Isso pode parecer a escolha óbvia ao iniciar com o TypeScript, mas imploro que você use anotações com moderação e deixe o TypeScript descobrir os tipos para você. Por quê? Deixe-me explicar o que realmente é uma anotação de tipo.

Uma anotação de tipo é uma maneira de expressar onde os contratos devem ser verificados. Se você adicionar uma anotação de tipo a uma declaração de variável, você instrui o compilador a verificar se os tipos correspondem durante a atribuição.

type Person = {
  name: string,
  age: number
}

const me: Person = createPerson()

Se createPerson retornar algo que não seja compatível com Person, o TypeScript apresentará um erro. Faça isso se você realmente quiser ter certeza de que está lidando com o tipo certo aqui.

Além disso, a partir desse momento, me é do tipo Person, e o TypeScript irá tratá-lo como Person. Se houver mais propriedades em me, por exemplo. uma profession, o TypeScript não permitirá que você os acesse. Não é definido em Person.

Se você adicionar uma anotação de tipo ao valor de retorno de uma assinatura de função, diga ao compilador para verificar se os tipos correspondem no momento em que você retornar esse valor.

function createPerson(): Person {
  return { name: "Stefan", age: 39 }
}

Se eu retornar algo que não corresponde a Person, o TypeScript dará erro. Faça isso se quiser ter certeza absoluta de que retornou o tipo correto. Isso é especialmente útil se você estiver trabalhando com funções que constroem objetos grandes de várias fontes.

Se você adicionar uma anotação de tipo aos parâmetros de uma assinatura de função, você diz ao compilador para verificar se os tipos correspondem no momento em que você passa os argumentos.

function printPerson(person: Person) {
  console.log(person.name, person.age)
}

printPerson(me)

Esta é, na minha opinião, a anotação de tipo mais importante e inevitável. Todo o resto pode ser inferido.

type Person = {
  name: string,
  age: number
}

// Inferido!
// return type is { name: string, age: number }
function createPerson() { 
  return { name: "Stefan", age: 39}
}

// Inferido!
// me is type of { name: string, age: number}
const me = createPerson() 

// Anotado! Você terá que checar se os tipos são compatíveis
function printPerson(person: Person) {
  console.log(person.name, person.age)
}

// All works
printPerson(me) 

Sempre use anotações de tipo com parâmetros de função. É aqui que você deve verificar seus contratos. Isso não é apenas muito mais conveniente, mas também vem com uma tonelada de benefícios. Você recebe, por exemplo polimorfismo de graça.

type Person = {
  name: string,
  age: number
}

type Studying = {
  semester: number
}

type Student = {
  id: string,
  age: number,
  semester: number
}

function createPerson() { 
  return { name: "Stefan", age: 39, semester: 25, id: "XPA"}
}

function printPerson(person: Person) {
  console.log(person.name, person.age)
}

function studyForAnotherSemester(student: Studying) {
  student.semester++
}

function isLongTimeStudent(student: Student) {
  return student.age - student.semester / 2 > 30 && student.semester > 20
}

const me = createPerson() 

// All work!
printPerson(me)
studyForAnotherSemester(me)
isLongTimeStudent(me)

Student, Person e Studying têm alguma sobreposição, mas não estão relacionados entre si. createPerson retorna algo compatível com todos os três tipos. Se tivéssemos anotado demais, precisaríamos criar muito mais tipos e muito mais verificações do que o necessário, sem nenhum benefício.

Ao aprender TypeScript, não confiar muito em anotações de tipo também lhe dá uma boa noção do que significa trabalhar com um sistema de tipo estrutural.

Erro 3: Confundir tipos com valores

TypeScript é um superconjunto de JavaScript, o que significa que adiciona mais coisas a uma linguagem já existente e definida. Com o tempo, você aprende a identificar quais partes são JavaScript e quais são TypeScript.

Realmente ajuda ver o TypeScript como essa camada adicional de tipos em JavaScript comum. Uma fina camada de meta-informação, que será removida antes que seu código JavaScript seja executado em um dos tempos de execução disponíveis. Algumas pessoas até falam sobre o código TypeScript “apagar para JavaScript” uma vez compilado.

Sendo o TypeScript essa camada em cima do JavaScript, também significa que diferentes sintaxes contribuem para diferentes camadas. Enquanto uma função ou constante cria um nome na parte JavaScript, uma declaração de tipo ou uma interface contribui com um nome na camada TypeScript. Por exemplo:

// Collection faz parte do TypeScript! --> tipo
type Collection<T> = {
  entries: T[]
}

// printCollection faz parte do JavaScript! --> valor
function printCollection(coll: Collection<unknown>) {
  console.log(...coll.entries)
}

Também dizemos que nomes ou declarações contribuem com um tipo ou um valor. Como a camada de tipo está no topo da camada de valor, é possível consumir valores na camada de tipo, mas não vice-versa. Também temos palavras-chave explícitas para isso.

// um valor
const person = {
  name: "Stefan"
}

// um tipo
type Person = typeof person;

typeof cria um nome disponível na camada de tipo da camada de valor abaixo.

Fica irritante quando há tipos de declaração que criam tipos e valores. As classes, por exemplo, podem ser usadas na camada TypeScript como um tipo, bem como em JavaScript como um valor.

// declaração
class Person {
  name: string

  constructor(n: string) {
    this.name = n
  }
}

// valor
const person = new Person("Stefan")

// tipo
type PersonCollection = Collection<Person>

function printPersons(coll: PersonCollection) {
  //...
}

E as convenções de nomenclatura enganam você. Normalmente, definimos classes, tipos, interfaces, enumerações, etc. com a primeira letra maiúscula. E mesmo que possam contribuir com valores, com certeza contribuem com tipos. Bem, até que você escreva funções em maiúsculas para o seu aplicativo React, pelo menos.

Se você está acostumado a usar nomes como tipos e valores, vai coçar a cabeça se de repente receber um bom e velho TS2749: ‘YourType’ refere-se a um valor, mas está sendo usado como um erro de tipo.

type PersonProps = {
  name: string
}

function Person({ name }: PersonProps) {
  return <p>{name}</p>
}

type Collection<T> = {
  entries: T
}

type PrintComponentProps = {
  collection: Collection<Person> // ERROR! 
  // 'Person' se refere a um valor, mas está sendo usado como tipo
}

É aqui que o TypeScript pode ficar realmente confuso. O que é um tipo, o que é um valor, por que precisamos separar isso, por que isso não funciona como em outras linguagens de programação? De repente, você se vê confrontado com chamadas typeof ou mesmo com o tipo auxiliar InstanceType, porque percebe que as classes na verdade contribuem com dois tipos (chocante!).

Portanto, é bom entender o que contribui com os tipos e o que contribui com o valor. Quais são os limites, como e em que direção podemos nos mover, e o que isso significa para suas digitações? Esta tabela, adaptada dos documentos do TypeScript, resume bem:

Ao aprender TypeScript, provavelmente é uma boa ideia se concentrar em funções, variáveis ​​e aliases de tipo simples (ou interfaces, se for mais o seu estilo). Isso deve lhe dar uma boa ideia sobre o que acontece na camada de tipo e o que acontece na camada de valor.

Erro 4: Ir com tudo no começo

Falamos muito sobre os erros que alguém pode cometer ao chegar no TypeScript vindo de uma linguagem de programação diferente. Para ser justo, este tem sido meu arroz com feijão por um bom tempo. Mas também há uma trajetória diferente: pessoas que escreveram muito JavaScript, de repente são confrontadas com outra ferramenta, às vezes muito irritante.

Isso pode levar a experiências muito frustrantes. Você conhece sua base de código como a palma da sua mão, de repente um compilador está lhe dizendo que não entende patavina e que você cometeu erros mesmo sabendo que seu software funcionará.

E você se pergunta como todo mundo pode gostar desse lixo. O TypeScript deve ajudá-lo a ser produtivo, mas tudo o que ele faz é lançar rabiscos vermelhos e distrativos sob seu código.

Todos nós já estivemos lá, não é?

E eu posso entender isso! O TypeScript pode ser muito barulhento, especialmente se você “apenas ligá-lo” em uma base de código JavaScript existente. O TypeScript quer ter uma noção de todo o seu aplicativo, e isso exige que você anote tudo para que os contratos se alinhem. Quão complicado.

Se você vem de JavaScript, eu diria que você deveria fazer uso dos recursos de adoção gradual do TypeScript. O TypeScript foi projetado para facilitar a adoção de um pouco, antes de mergulhar de cabeça:

  1. Pegue partes do seu aplicativo e mova-as para o TypeScript, em vez de mover tudo. TypeScript tem interoperabilidade JavaScript (allowJS)
  2. O TypeScript emite código JavaScript compilado mesmo quando o TypeScript encontra erros em seu código. Você precisa desativar a emissão de código explicitamente usando o sinalizador noEmitOnError. Isso permite que você ainda envie mesmo que seu compilador grite com você
  3. Use o TypeScript escrevendo arquivos de declaração de tipo e importando-os via JSDoc. Este é um bom primeiro passo para obter mais informações sobre o que está acontecendo dentro de sua base de código.
  4. Use any em qualquer lugar em que seria muito cansativo ou muito trabalho. Ao contrário das crenças populares, usar any é absolutamente correto, desde que seja usado explicitamente

Confira a referência tsconfig para ver quais sinalizadores de configuração estão disponíveis. O TypeScript foi projetado para adoção gradual. Você pode usar quantos tipos quiser. Você pode deixar grandes partes de seu aplicativo em JavaScript, e isso definitivamente deve ajudá-lo a começar.

Ao aprender TypeScript como desenvolvedor JavaScript, não exija muito de si mesmo. Tente usá-lo como documentação embutida para raciocinar melhor sobre seu código e estender/melhorar isso.

Erro 5: Aprenda o TypeScript errado

Novamente, muito inspirado em Como não aprender Rust. Se seu código precisar usar uma das seguintes palavras-chave, você provavelmente está no canto errado do TypeScript ou muito mais longe do que deseja:

  • namespace
  • declare
  • module
  • <reference>
  • abstract
  • unique

Isso não significa que essas palavras-chave não contribuam com algo muito importante e sejam necessárias para uma variedade de casos de uso. Ao aprender TypeScript, você não quer trabalhar com eles no começo.

E é isso! Estou curioso para saber como você aprendeu o TypeScript e quais obstáculos você encontrou ao começar. Além disso, você conhece outras coisas que podem ser erros comuns ao aprender TypeScript? Avise! Estou ansioso para ouvir suas histórias.”

Publicado originalmente como “How not to learn TypeScript” em 10 de janeiro de 2022. Traduzido e republicado com autorização do autor.