TypeScript: Entendendo a notação de tipos

Como ler, entender e aplicar a notação de tipos estáticos

Image for post
Image for post
Conhecer e entender os dialetos utilizados facilitam a criatividade e comunicação

1. O que você vai aprender

interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U): U;
···
}

Se você acha que isso é enigmático — então eu concordo com você. Mas (como espero provar) essa notação é relativamente fácil de aprender. E uma vez entendido, fornece resumos imediatos, precisos e abrangentes de como o código se comporta. Não há necessidade de ler comentários longos em portuglês: “TODO: Amanhã rebase o fix do upstream” 😂.

2. Experimentando com os códigos de exemplo

3. Especificando a abrangência da verificação de tipos

  • --noImplicitAny: Se o TypeScript não puder inferir um tipo, você deverá especificá-lo. Isso se aplica principalmente a parâmetros de funções e métodos: com essas configurações, você deve anotá-las.
  • --noImplicitThis: Reclama se o tipo de this não estiver claro.
  • --alwaysStrict: Use o modo estrito do JavaScript sempre que possível.
  • --strictNullChecks: null não faz parte de nenhum tipo (diferente do seu próprio tipo null) e deve ser explicitamente mencionado se for um valor aceitável.
  • --strictFunctionTypes: verificações mais fortes para tipos de função.
  • --strictPropertyInitialization: Se uma propriedade não puder ter o valor undefined, ela deverá ser inicializada no constructor.

Mais informações: capítulo “Compiler Options” no manual do TypeScript.

4. Tipos

  • Undefined: o conjunto com o único elemento undefined.
  • Null: o conjunto com o único elemento null.
  • Boolean: o conjunto com os dois elementos false e true.
  • Number: o conjunto de todos os números.
  • String: o conjunto de todas as strings.
  • Symbols: o conjunto de todos os símbolos.
  • Objects: o conjunto de todos os objetos (que inclui funções e arrays).

Todos esses tipos são dinâmicos : você pode usá-los em tempo de execução.

O TypeScript traz uma camada adicional ao JavaScript: tipos estáticos. Estes só existem quando compilando ou verificando os tipos no código fonte. Cada local de armazenamento (variável ou propriedade) tem um tipo estático que prevê seus valores dinâmicos. A verificação de tipo garante que essas previsões se realizem. E há bastante coisas que você pode verificar estaticamente (sem executar o código). Se, por exemplo, o parâmetro x de uma função f(x), deve ter o tipo estático number, a chamada da função f(‘abc’), será ilegal, porque o parâmetro ’abc’ tem o tipo estático incorreto.

5. Anotações de Tipos

let x: number;

Você pode se perguntar se a inicialização undefined de x não viola o tipo estático. O TypeScript contorna esse problema não permitindo que você leia x antes de atribuir um valor a ele.

6. Inferência de Tipos

let x = 123;

Em seguida, o TypeScript infere que x tem o tipo estático number.

7. Descrevendo Tipos

Tipos básicos são expressões de tipo válidas:

  • Tipos estáticos para tipos dinâmicos do JavaScript: undefined, null, boolean, number, string, symbol, object.
  • Tipos específicos do TypeScript: any (o tipo de todos os valores), etc.

Perceba que “undefined as a value” e “undefined as a type” são ambos escritos como undefined. Dependendo de onde você o usa, ele é interpretado como um valor ou como um tipo. O mesmo acontece para null.

Você pode criar mais expressões de tipo combinando tipos básicos por meio de operadores de tipo , que combinam tipos de forma semelhante à combinação de operadores union(∪) e intersection(∩).

As seções a seguir explicam alguns dos operadores de tipos que o TypeScript oferece.

8. Tipos em Array

  • Lists: Todos os elementos têm o mesmo tipo. O comprimento do array varia.
  • Tuple: O comprimento do array é fixo. Os elementos não têm necessariamente o mesmo tipo.

8.1 Arrays como Lists

let arr: number[] = [];
let arr: Array<number> = [];

Normalmente, o TypeScript pode inferir o tipo de uma variável, se houver uma atribuição. Neste caso, você realmente tem que ajudá-lo, porque com um array vazio, não é possível determinar o tipo dos elementos.

Voltaremos à notação Array mais tarde.

8.2 Arrays como Tuples

let point: [number, number] = [7, 5];

Nesse caso, você não precisa da anotação de tipo.

Outro exemplo de tuple é o resultado de Object.entries(obj): é um array com um para [chave, valor], para cada propriedade de obj:

> Object.entries({a:1, b:2})
[ [ 'a', 1 ], [ 'b', 2 ] ]

O tipo do resultado de Object.entries() é:

Array<[string, any]>

9. Tipos em Funções

(num: number) => string

Esse tipo inclui todas as funções que aceitam um único parâmetro, um número e retornam uma string. Vamos usar esse tipo em uma anotação de tipo (perceba que estamos atribuindo o valor String do JavaScript, que é usada como uma função aqui):

const func: (num: number) => string = String;

O código a seguir é um exemplo mais realista:

function stringify123(callback: (num: number) => string) {
return callback(123);
}

Estamos usando um tipo de função para descrever o parâmetro callback de stringify123(). Devido a este tipo de anotação, o TypeScript rejeita a seguinte chamada de função.

f(Number);

Mas aceita a seguinte:

f(String);

9.1 Tipos de resultados de declarações de função

function stringify123(callback: (num: number) => string): string {
const num = 123;
return callback(num);
}

E quando não temos nada retornado?

Esse é o tipo especial chamado void.

void é um tipo de resultado especial para funções, ele diz ao TypeScript que a função sempre retorna undefined (explicitamente ou implicitamente):

function f1(): void { return undefined } // OK
function f2(): void { } // OK
function f3(): void { return 'abc' } // error

9.2 Parâmetros Opcionais

function stringify123(callback?: (num: number) => string) {
const num = 123;
if (callback) {
return callback(num); // (A)
}
return String(num);
}

Se você executar o TypeScript com –strict, ele só permitirá que você faça a chamada de função na linha A, se você verificar de antemão que callback não foi omitido.

E os valores padrão de um parâmetro?

O TypeScript suporta valores padrão de parâmetro ES6:

function createPoint(x=0, y=0) {
return [x, y];
}

Valores padrão tornam os parâmetros opcionais. Normalmente você pode omitir anotações de tipo, porque o TypeScript pode inferir os tipos. Por exemplo, ele pode inferir que ambos, x e y, têm o tipo number.

Se você quisesse adicionar anotações de tipo, seria o seguinte:

function createPoint(x:number = 0, y:number = 0) {
return [x, y];
}

9.3 Tipos do operador rest

function joinNumbers(...nums: number[]): string {
return nums.join('-');
}
joinNumbers(1, 2, 3); // '1-2-3'

10. Realizando Uniões de Tipos (Union Types)

let x = null;
x = 123;

O tipo de x pode ser descrito como null | number:

let x: null|number = null;
x = 123;

O resultado da expressão de tipo foo | bar é a união teórica dos conjuntos de tipos de foo e bar (que, como vimos anteriormente, JavaScript possui vários conjuntos de tipos).

Vamos reescrever a função stringify123(): Desta vez, não queremos que o parâmetro callback seja opcional. Ele deve sempre ser mencionado. Se, quem estiver chamando não quiser fornecer uma função callback, eles precisam passar explicitamente null. Isso é implementado da seguinte maneira.

function stringify123(
callback: null | ((num: number) => string)) {
const num = 123;
if (callback) { // (A)
return callback(123); // (B)
}
return String(num);
}

Perceba que, mais uma vez, temos que verificar se callback é realmente uma função (linha A), antes de realizar a chamada da função na linha B. Sem a verificação, o TypeScript reportaria um erro.

10.1 Opcional vs undefined | T

A principal diferença é que você pode omitir os parâmetros opcionais:

function f1(x?: number) { }
f1(); // OK
f1(undefined); // OK
f1(123); // OK

Mas você não pode omitir parâmetros do tipo undefined | T:

function f2(x: undefined | number) { }
f2(); // error
f2(undefined); // OK
f2(123); // OK

10.2 Os valores null e undefined geralmente não estão incluídos nos tipos

Por outro lado, no TypeScript, undefined e null são tratados separadamente, pelo chamado tipos separados (disjoint types). Você precisa de um tipo de união como undefined | string e null | string, se você quiser permitir essas operações.

11. Tipos em Objetos

  • Records: uma quantidade fixa de propriedades que são conhecidas no momento do desenvolvimento. Cada propriedade pode ter um tipo diferente.
  • Dictionaries: uma quantidade arbitrária de propriedades cujos nomes não são conhecidos no momento do desenvolvimento. Todas as chaves de propriedade (strings e/ou símbolos) possuem o mesmo tipo, assim como os valores da propriedade.

Iremos ignorar objetos-como-dicionários neste artigo. De qualquer forma, ES6 Map geralmente é uma opção melhor para dicionários.

11.1 Objetos como Records

interface Point {
x: number;
y: number;
}

Uma grande vantagem do sistema de tipos do TypeScript é que ele funciona estruturalmente, não nominalmente. Ou seja, a interface Point corresponde a todos os objetos que possuem a estrutura apropriada:

function pointToString(p: Point) {
return `(${p.x}, ${p.y})`;
}
pointToString({x: 5, y: 7}); // '(5, 7)'

Em contraste, o sistema de tipo nominal do Java requer classes para implementar interfaces.

11.2 Propriedades Opcionais

interface Person {
name: string;
company?: string;
}

11.3 Métodos

interface Point {
x: number;
y: number;
distance(other: Point): number;
}

12. Variáveis de Tipo e Tipos Genéricos

  • Valores existem no nível do objeto .
  • Tipos existem em um nível meta.

Similarmente:

  • Variáveis ​​normais existem no nível do objeto.
  • Existem também variáveis ​​de tipo, que existem no nível meta. São variáveis ​​cujos valores são tipos.

As variáveis ​​normais são introduzidas por meio de const, let, etc. As variáveis ​​de tipo são introduzidas meio de colchetes angulares (< >). Por exemplo, o código a seguir contém a variável de tipo T, introduzida pelo <T>:

interface Stack<T> {
push(x: T): void;
pop(): T;
}

Você pode ver que o parâmetro de tipo T aparece duas vezes dentro do corpo de Stack. Portanto, essa interface pode ser entendida intuitivamente da seguinte maneira:

  • Stack é uma pilha de valores que todos têm um determinado tipo T. Você deve preencher T sempre que mencionar Stack. Vamos ver como, a seguir.
  • O método .push() aceita valores do tipo T.
  • Método .pop() retorna valores do tipo T.

Se você usar Stack, você deve atribuir um tipo para T. O código a seguir mostra uma pilha fictícia, cuja única finalidade é corresponder à interface.

const dummyStack: Stack<number> = {
push(x: number) {},
pop() { return 123 },
};

12.1 Exemplos: Map

const myMap: Map<boolean,string> = new Map([
[false, 'no'],
[true, 'yes'],
]);

12.2 Variáveis de Tipo para Funções

function id<T>(x: T): T {
return x;
}

Você usa essa função da seguinte maneira:

id<number>(123);

Devido à inferência de tipos, você também pode omitir o parâmetro type:

id(123);

12.3 Passando um parâmetro de Tipo

function fillArray<T>(len: number, elem: T) {
return new Array<T>(len).fill(elem);
}

A variável de tipo T aparece três vezes neste código:

  • fillArray<T>: introduz a variável de tipo.
  • elem: T: usa a variável de tipo, pegando sua referência do argumento passado.
  • Array<T>: passa T para o construtor do Array.

Isso significa que não precisamos especificar explicitamente o tipo T de Array — ele é inferido no parâmetro elem:

const arr = fillArray(3, '*');
// Inferred type: string[]

13. Conclusão

interface Array<T> {
concat(...items: Array<T[] | T>): T[];
reduce<U>(
callback: (state: U, element: T, index: number, array: T[]) => U,
firstState?: U): U;
···
}

Esta é uma interface para um Array cujos elementos são do tipo T e que temos que preencher sempre que utilizamos esta interface:

  • O método .concat() tem zero ou mais parâmetros (definidos pelo operador rest). Cada um desses parâmetros tempo o tempo T[] | T. Ou seja, é um array de valores T ou um valor único T.
  • O método .reduce() introduz sua própria variável de tipo U. O U expressa o fato de que as entidades a seguir têm o mesmo tipo (que você não precisa especificar, ele é inferido automaticamente):
  • O parâmetro state do callback() (que é uma função)
  • O resultado de callback()
  • O parâmetro opcional firstState de .reduce()
  • O resultado de .reduce()

callback() também obtém um parâmetro element: T, cujo tipo tem o mesmo tipo T dos elementos do Array, um parâmetro index, que é um número e um parâmetro array com valores T.

14. Leitura adicional

⭐️ Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store