Entendendo o básico de TypeScript

Arco Tech
Geekie Educação
Published in
10 min readMar 5, 2024
Imagem gerada com ChatGPT-4 e DALL-E

TypeScript é uma linguagem de programação baseada em JavaScript, mas com a adição de anotações de tipos. Neste artigo, vamos explicar os conceitos de tipagem, mostrar a sintaxe básica da linguagem, como o TypeScript se integra com React, alguns erros comuns e boas práticas. Dessa forma, seu código pode ficar menos sujeito a erros e mais fácil de entender.

Tipagem e seus benefícios

JavaScript é uma linguagem dinamicamente tipada. Isso significa que as atribuições de tipos acontecem apenas no momento de execução. Por exemplo, no seguinte código:

const sortAscending = numbers => numbers.sort((a, b) => a - b);

A sintaxe não exige definição de tipos para o numbers. A função sortAscending é implicitamente feita para receber um array de inteiros, mas um código incorreto como sortAscending("abcd") só vai causar erro quando a função for de fato chamada, na execução do numbers.sort.

Isso é diferente de linguagens estaticamente tipadas, como é o caso de Java por exemplo. Se houver uma função similar:

public static void sortAscending(int[] numbers) {
Arrays.sort(numbers);
}

Um código sortAscending("abcd") não poderá nem ser compilado, pelo tipo errado passado para a função.

TypeScript vai para o meio-termo: é uma linguagem gradualmente tipada. É possível definir tipos em partes do programa, que serão verificados estaticamente (antes de o código ser rodado). Mas isso é opcional, e em outras partes a tipagem acontece apenas em tempo de execução.

A função sortAscending feita em JavaScript mais acima também é código válido TypeScript, mas dessa forma a verificação de tipos fica para a execução do código. Também é possível escrevê-la com tipos:

const sortAscending = (numbers: number[]) => numbers.sort((a, b) => a - b);

Assim:

  • Fica claro para quem lê a função que ela só pode receber arrays numéricos;
  • Evita-se descobrir que o código quebra apenas no momento da execução. Uma chamada sortAscending("abcd"), que produz um erro, pode exibir um aviso no próprio editor de código. Assim, o problema pode ser corrigido mais rapidamente do que se o código precisasse ser rodado;
  • Evitam-se bugs por restrições que não estavam claras à primeira vista. Uma chamada sortAscending(["b", "d", "a", "c"]) não quebra, mas produz ["b", "d", "a", "c"], que não está em ordem alfabética como se poderia esperar. Isso porque a função de comparação passada para a sort supõe que os valores do array são sempre numéricos. A função tipada permite que quem desenvolve perceba o problema durante a escrita do código em vez de vê-lo acontecendo em produção.

E, com a tipagem gradual, é possível converter um codebase existente em JavaScript para TypeScript progressivamente.

Sintaxe básica

Vamos mostrar algumas construções de TypeScript para que você consiga escrever e entender código na linguagem.

Tipos básicos

Veja abaixo como especificar alguns tipos básicos para as variáveis:

boolean: Verdadeiro ou falso.

let isDone: boolean = false;

number: Números inteiros ou de ponto flutuante.

let decimal: number = 6;

string: Texto.

let color: string = 'blue';

array: Arrays, que podem ser escritos de duas formas.

let list: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3]; // Sintaxe de array genérico

tuple: Arrays com tipos e número de elementos fixos.

let x: [string, number];
x = ['hello', 10];

void: Usado em funções que não devolvem nenhum tipo de dado.

function warnUser(): void {
console.log('Aqui é um log de aviso!');
}

any: Pode ser qualquer de qualquer tipo. Evite usar.

let notSure: any = 4;

Tipos literais

Os tipos literais no TypeScript são uma maneira de definir tipos que representam um valor dentro de um conjunto limitado. Com eles, é possível restringir uma variável a valores específicos, fazendo com que seu código seja mais preciso e expressivo.

Exemplo: você pode especificar que uma variável deve ter uma string específica como seu valor.

let direction: 'north' | 'south' | 'east' | 'west';
direction = 'north'; // Válido
direction = 'north-east'; // Erro: O Typescript avisa que o valor "north-east" não pode ser atribuido a variável pois este valor não foi especificado em sua tipagem.

Union types

Os union types permitem que você especifique que uma variável possa conter mais de um tipo.

let myVar: string | number;
myVar = 'Hello'; // Válido
myVar = 42; // Válido
myVar = [42]; // Erro: O typescript avisa que o valor atribuido não contém a tipagem esperada, já que o tipo de "[42]" seria "number[]".

Neste caso, com o union type estamos permitindo que somente valores do tipo string ou number sejam atribuídos à variável myVar. Caso tente atribuir qualquer tipo de valor que não seja desse tipo, como na última linha do exemplo, o TypeScript deve apontar o erro em sua atribuição de valor à variável.

Enum

O enum permite definir um conjunto de constantes nomeadas, por exemplo:

enum Color {
Red,
Green,
Blue,
}
let myColor: Color = Color.Green;

Nesse caso Colors.Red teria o valor 0, Colors.Green 1 e Colors.Blue 2.

Esse uso é parecido com os union types: as variáveis podem ter um valor dentre um conjunto pré-determinado. Mas, diferentemente de outras features de TypeScript, enums existem também em runtime, o que aumenta o tamanho do bundle JavaScript gerado.

O uso e o debug dos union types também tende a ser mais simples. Porém, existem casos em que o uso de enums se justifica, como o seguinte:

enum SaveErrorCode {
PermissionDenied = 423,
DiskSpaceFull = 534,
ValidationError = 968,
}

Como o valor a que cada item do enum está associado não é óbvio, o enum melhora a legibilidade. É mais fácil de entender um código if (error === SaveErrorCode.DiskSpaceFull) do que um if (error == 1003).

as const

A asserção as const permite que você trate elementos como constantes literais, fazendo com que objetos, arrays e valores literais sejam tratados como imutáveis e com tipos literais precisos.

let myVar = { text: 'hello' } as const;
// O tipo da variável myVar é agora "{ readonly text: "hello" }", em vez de "{ text: string }"

Quando usado com arrays, as const previne a adição de novos elementos ao array ou a alteração de elementos existentes.

let myArray = [10, 20] as const;
// O tipo da variável myArray é agora "readonly [10, 20]"

Esses conceitos formam a base do TypeScript, o que pode auxiliar você a escrever um código JavaScript mais seguro e fácil de entender.

Como o TypeScript se integra com o React?

Uma das grandes vantagens do TypeScript é a forma como ele se integra com o React, nos ajudando a trazer segurança e eficiência ao desenvolvimento. Abaixo alguns pontos interessantes que vale citar:

  • Tipagem forte: o TypeScript permite que você defina tipos para props, estados e eventos. Isso ajuda a evitar erros, como passar um tipo errado de prop para um componente;
  • Componentes e props: você pode definir tipos para as props dos componentes. Isso melhora a documentação e a verificação de erros em tempo de compilação;
  • Hooks: com TypeScript você também pode definir tipagem de hooks do React, como useState e useEffect, permitindo que você especifique o tipo de valor que o estado deve ter por exemplo;
  • Autocomplete: o TypeScript melhora significativamente o autocomplete, o que traz um ganho muito grande na produtividade e manutenção do código.

Então, podemos dizer que, utilizando o TypeScript em um projeto junto com o React, você tem uma melhora na experiência de desenvolvimento ao adicionar tipagem estática, o que reduz muito os erros e traz ganhos também na qualidade do código.

Definindo tipos para props

A tipagem forte do TypeScript no contexto do React ajuda a garantir que os componentes recebam e utilizem os dados corretos. Veja os exemplos abaixo:

Imagine o componente Hello que aceita uma prop chamada name do tipo string.

Sem TypeScript

function Hello({ nome }) {
return <div>Olá, {nome}!</div>;
}

Com TypeScript

type HelloProps = {
name: string;
};
function Hello({ name }: HelloProps) {
return <div>Olá, {name}!</div>;
}

Aqui definimos um tipo HelloProps que especifica que name deve ser uma string. Se tentarmos passar um valor que seja um número ou qualquer outro tipo para name, o TypeScript mostrará um erro durante o desenvolvimento.

Definindo tipos para eventos

Você também pode definir tipos para eventos, como eventos de clique por exemplo:

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Lógica do clique
};

Nesse caso, React.MouseEvent<HTMLButtonElement> especifica que o evento se refere a um clique do mouse em um elemento <button>.

Assim, é possível evitar erros que podem ser identificados durante o desenvolvimento, antes mesmo da execução do código. Além disso, outro benefício notável é o recurso de autocomplete, onde o editor de código aproveita as informações de tipagem definidas para sugerir completamentos automáticos, acelerando e facilitando o processo de desenvolvimento.

Erros comuns

No TypeScript, algumas mensagens de erro são comuns, especialmente para quem está começando ou até mesmo para desenvolvedores experientes enfrentando casos de uso complexos. Veja alguns exemplos comuns seguidos de uma breve explicação:

Erro de tipagem: tentar atribuir valores a uma variável de um tipo não esperado.

let myNumber: number = 'string';
// Erro: Type 'string' is not assignable to type 'number'.

Erro ao acessar propriedades de null ou undefined: tentar acessar propriedades de valores que podem ser null ou undefined gera erro, pois não há garantia de que a propriedade exista nesses casos.

let user = { name: 'John' };
console.log(user.age);
// Erro: Property 'age' does not exist on type '{ name: string; }'.

Argumentos faltantes em funções: esse erro ocorre quando uma função espera receber certos argumentos, mas é chamada sem eles. O TypeScript exige que todos os argumentos sejam fornecidos, para garantir a integridade da chamada.

function greet(name: string) {
return `Hello, ${name}!`;
}
greet();
// Erro: An argument for 'name' was not provided.

Incompatibilidade de tipos utilizando union types: ocorre quando um valor não corresponde a nenhum tipo especificado, violando as restrições definidas pelo union type.

let id: string | number;
id = true;
// Erro: Type 'boolean' is not assignable to type 'string | number'.

Erro de tipo literal: ocorre quando um valor atribuído não corresponde exatamente ao tipo literal especificado, como atribuir up a um tipo "left" | "right" .

let literal: 'left' | 'right';
literal = 'up';
// Erro: Type '"up"' is not assignable to type '"left" | "right"'.

Erro de tipos em arrays: ocorre porque o array espera elementos do tipo number, mas ao tentar inserir um elemento que seja uma string, acaba violando a tipagem do array.

let numbers: number[] = [1, 2, 3];
numbers.push('4');
// Erro: Argument of type 'string' is not assignable to parameter of type 'number'.

Uso incorreto de any: o uso excessivo de any pode levar a um código menos seguro.

let vague: any = 4;
vague.method(); // Compila, mas retorna um erro em tempo de execução.

Estes erros destacam a importância de entender bem os tipos do TypeScript e como aplicá-los corretamente para aproveitar os benefícios da tipagem estática, evitando assim problemas comuns de tipagem e lógica.

Algumas boas práticas

Agora vamos falar de algumas boas práticas e dúvidas que podem surgir no uso de TypeScript.

Tipagem implícita e explícita

Diferentemente de outras linguagens estaticamente tipadas, não é necessário sempre dizer explicitamente o tipo de uma variável TypeScript para que ela esteja tipada corretamente. Por exemplo, no código:

let n = 1;

A variável n já é inferida automaticamente como sendo um number, de forma que fica desnecessário escrever n: number.

A tipagem explícita pode inclusive não ser uma boa prática. Por exemplo, em callbacks passados para funções de arrays:

untypedArray.map(element => f(element))

O editor pode alertar sobre a variável element não estar tipada, e pode ter a vontade de adicionar um tipo a ela. Mas, se o array original estiver tipado corretamente, o map infere os tipos do callback corretamente. Então a origem do problema é o untypedArray não ter o tipo correto, e não o parâmetro do callback.

Contudo, um caso em que pode compensar o uso de tipagem explícita é o de funções exportadas (não usadas apenas internamente num arquivo). Os tipos dos parâmetros e do valor devolvido pela função ajudam a entender como usá-la, além de servirem como uma garantia mais forte de que a API da função é clara e vai ser preservada se vier a ser modificada.

Evitar perdas de informação sobre os tipos em funções

A seguinte função obtém o primeiro elemento de um array arbitrário:

function getFirst(array: any[]) {
return array[0];
}

A tipagem diz que o array pode ter elementos quaisquer. Isso é desejável pois a função funciona para arrays de tipos variados, mas existe um problema com essa abordagem.

Ao fazer uma chamada como getFirst([1, 2, 3]), o valor resultante é do tipo any em vez de ser number. A informação de que os elementos de [1, 2, 3] eram números se perde após a chamada da função.

Isso pode ser resolvido com tipos genéricos:

function getFirst<ElementType>(array: ElementType[]) {
return array[0];
}

Essa sintaxe usa uma “variável de tipo” ElementType, que define o tipo dos elementos do array. Dessa forma o valor devolvido pela função é do tipo que esses elementos forem: getFirst([1, 2, 3]) passa a ser um number.

Tipagem de testes

Existem benefícios em adicionar tipos também nos testes automatizados, além de na própria aplicação. Por exemplo, considere o seguinte componente React e um teste para ele:

const ConfirmationPrompt = ({ onConfirmPress, onCancelPress }) => (
<div>
<div onClick={onConfirmPress}>Confirmar</div>
<div onClick={onCancelPress}>Cancelar</div>
</div>
);
describe('ConfirmationPrompt', () => {
const getComponent = props => <ConfirmationPrompt {...props} />;
it('calls onCancelPress but not onConfirmPress when the cancel button is pressed', () => {
const onComfirmPress = jest.fn();
const onCancelPress = jest.fn();
const component = render(getComponent({ onComfirmPress, onCancelPress }));
fireEvent.press(component.getByText('Cancelar'));
expect(onCancelPress).toHaveBeenCalled();
expect(onComfirmPress).not.toHaveBeenCalled();
});
});

O ConfirmationPrompt tem dois botões: um “Confirmar”, que chama a prop onConfirmPress, e um “Cancelar”, que chama a onCancelPress. E há um teste que deveria confirmar que, ao clicar em “Cancelar”, apenas a onCancelPress é chamada.

O teste passa, mas ele não testa corretamente tudo que deveria. Consegue perceber o motivo?

Existe um erro de digitação na prop onComfirmPress (que está com “m” no lugar de “n") passada para a função getComponent.

O teste passa porque a verificação feita é de que a onComfirmPress não é chamada, o que é de fato verdadeiro pois a prop sequer é passada para o componente. Mas se, por acaso, o componente tivesse um bug que fizesse com que a onConfirmPress também fosse chamada ao clicar em “Cancelar”, o teste não pegaria esse problema.

É possível definir tipos que evitem esse problema, e assim existiria um aviso ao passar props incorretas para a função getComponent. O TypeScript ajuda a garantir que os inputs são corretos e que os testes de fato testam o que deveriam.

O TypeScript permite que você continue programando em JavaScript mas tenha vantagens como uma melhor experiência no desenvolvimento, prevenção de erros e autocomplete aprimorado. Se você não usa TypeScript, ficou com vontade de usar? Se já usa e tiver alguma sugestão sobre o artigo, deixe seu comentário!

[Este artigo foi escrito por Gabriel Guilhoto (@gabrielguilhoto) da Geekie e Lucas Ruy (@lucasruy) da ArcoTech. Também usamos como referência documentações internas da Geekie e a documentação oficial do TypeScript.]

--

--