TypeScript: Generics para pessoas que desistiram de entender Generics
Tags: ptbr, typescript, webdev, beginners
Published: 2020-01-15 20:58:05

[_Créditos da Imagem_](https://www.dotnetcurry.com/typescript/1439/typescript-generics)

Você já ficou confuso com o código a seguir? Se perguntando "Porquê TypeScript Generics é tão difícil?"

```tsx
// Já ficou confuso com código como esse?
function makePair<
  F extends number | string,
  S extends boolean | F
>()
```

Se você é (1) novo no TypeScript, (2) novo com Generics e (3) lutando para entender as _generics_, então você é exatamente como eu quando eu estava aprendendo Java há 13 anos.

Como o TypeScript, a linguagem de programação Java suporta Generics. Quando eu estava estudando Java na faculdade, eu era um programador iniciante, e as Generics eram muito difíceis para mim. Então desisti de aprender na época e os usei sem saber o que estava fazendo. Eu não entendi Generics até conseguir um emprego em período integral depois da faculdade.

Talvez você seja como eu de 13 anos atrás e tenha sentido que as Generics em TypeScript são muito difíceis. Nesse caso, este tutorial é para você! Vou tentar ajudá-lo!

# Vamos falar sobre makeState()

Primeiro, criei uma função chamada `makeState()` abaixo. Usaremos essa função para falar sobre genéricos.

```tsx
function makeState() {
  let state: number
  function getState() {
    return state
  }
  function setState(x: number) {
    state = x
  }
  return { getState, setState }
}
```

Quando você executa `makeState()`, ele retorna duas funções: `getState()` e `setState()`. Você pode usar essas funções para definir e obter a variável chamada `state`.

Vamos experimentar! O que é impresso no console quando você executa o código a seguir?

```tsx
const { getState, setState } = makeState()
setState(1)
console.log(getState()) // 1

setState(2)
console.log(getState()) // 2
```

É mostrado 1 e depois 2. Muito simples, certo?

# E se usarmos uma string?

Agora, ao invés de números como 1 ou 2, o que acontece se usarmos uma string como 'foo'? Tente adivinhar primeiro...

```tsx
const { getState, setState } = makeState()

// O que acontece se usarmos uma string?
setState('foo')
console.log(getState())
```

O TypeScript não irá compilar, porque `setState()` espera um número:

```tsx
function makeState() {
  let state: number
  function getState() {
    return state
  }
  // Aqui o setState() espera um número
  function setState(x: number) {
    state = x
  }
  return { getState, setState }
}
```

Para consertar isso, podemos alterar o tipo de `state` e `x` de `number` para `string`:

```tsx
function makeState() {
  // Mudamos para string
  let state: string
  function getState() {
    return state
  }
  // Aceita uma string
  function setState(x: string) {
    state = x
  }
  return { getState, setState }
}
```

Agora vai funcionar! 🥳

```tsx
const { getState, setState } = makeState()
setState('foo')
console.log(getState()) // 'foo'
```

# O nosso desafio é receber dois estados diferentes

Agora que entendemos o básico, eis um desafio para você:

> Podemos modificar `makeState()` de modo que ele possa criar dois estados diferentes? Um que permita apenas números e o outro que permita apenas strings?

Aqui está o que eu quero dizer:

```tsx
// Queremos modificar makeState() para suportar
// a criação de dois estados diferentes:

// Um que permita números...
const numState = makeState()
numState.setState(1)
console.log(numState.getState()) // 1

// E outro que permita strings.
const strState = makeState()
strState.setState('foo')
console.log(strState.getState()) // foo
```

No começo, nosso primeiro makeState() criou estados somente com números e nosso segundo makeState() criou estados somente com string. No entanto, não foi possível criar ambos os estados.

Como podemos modificar o makeState() para alcançar nosso objetivo?

# Tentativa 1: Isso funciona?

Aqui está a primeira tentativa. Isto funciona?

```tsx
function makeState() {
  let state: number | string
  function getState() {
    return state
  }
  function setState(x: number | string) {
    state = x
  }
  return { getState, setState }
}
```

**Isso não funciona.** Você acabará criando um estado que permite números e strings, que não é o que queremos. Ao invés disso, queremos que o makeState() suporte a criação de dois estados diferentes: um que permita apenas números e o outro que permita apenas strings.

```tsx
// Não funciona porque o estado criado...
const numAndStrState = makeState()

// Permite números...
numAndStrState.setState(1)
console.log(numAndStrState.getState())

// E strings...
numAndStrState.setState('foo')
console.log(numAndStrState.getState())

// Isso NÃO é o que queremos. 
// Queremos criar um estado com apenas números
// e outro com apenas strings
```

# Tentativa 2: Usando Generics

É aqui que entram as Generics. Dê uma olhada abaixo:

```tsx
function makeState<S>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}
```

`makeState()` agora é definido como `makeState<S>()`. Você pode pensar em `<S>` como outra coisa que você deve passar ao chamar a função. Mas, ao invés de passar um valor, você passa um tipo para ele.

Por exemplo, você pode passar o tipo `number` como `S` quando você chama `makeState()`: 

```tsx
// Agora "S" é uma referência para "number" 
makeState<number>()
```

Então, dentro da definição da função `makeState()`, `S` se tornará `number`:

```tsx
...
// No corpo da função makeState()...
let state: S // <- number

function setState(x: S /* <- number */) {
  state = x
}
...
```

Porque `state` será `number` e `setState` aceita apenas `number`, ele cria um estado apenas de números.

```tsx
// Cria um estado de apenas números
const numState = makeState<number>()
numState.setState(1)
console.log(numState.getState())

numState.setState('foo') // irá falhar!
```

Por outro lado, para criar um estado somente de string, você pode passar `string` como `S` quando você chama `makeState()`:

```tsx
// Cria um estado de apenas strings
const strState = makeState<string>()
strState.setState('foo')
console.log(strState.getState())

strState.setState(1) // irá falhar!
```

Nota: Chamamos `makeState<S>()` de "Função Genérica", porque é literalmente genérica - você pode optar por torná-la somente número ou apenas string. E agora, você sabe o que é uma função genérica quando a função recebe um parâmetro de tipo!

```tsx
// `makeState` agora é uma função genérica
function makeState<S>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}
```

Você pode estar se perguntando: por que nomeamos o parâmetro de tipo como "S"?

Resposta: Na verdade, pode ser qualquer nome, mas geralmente as pessoas usam a primeira letra de uma palavra que descreve o que o tipo está representando. Nesse caso, eu escolhi "S" porque está descrevendo o tipo de "state". Os seguintes nomes também são comuns:

- T (para "type")
- E (para "element")
- K (para "key")
- V (para "value")

# Problema: Você pode criar um estado booleano!

Mas espere um minuto: se você passar boolean para S , você pode criar um estado somente booleano.

```tsx
// Cria um estado de apenas booleanos
const boolState = makeState<boolean>()
boolState.setState(true)
console.log(boolState.getState())
```

Talvez, queremos que isso não seja permitido. Suponha que não queremos que `makeState()` consiga criar estados que não sejam de número ou de strings (como booleanos). Como podemos garantir isso?

A solução: quando você declara `makeState()`, você altera o parâmetro de tipo `<S>` para `<S extends number | string>`. Essa é a única alteração que você precisa fazer.

```tsx
function makeState<S extends number | string>()
```

Ao fazer isso, quando você chama makeState(), você só pode passar number, string ou qualquer outro tipo que estenda number ou string como S.

Vamos ver o que acontece agora quando você tenta passar um booleano:

```tsx
function makeState<
  S extends number | string
>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}

// O que acontece se passarmos booleano para S?
const boolState = makeState<boolean>()
```

Isso resulta em um erro, que é o que queremos! Prevenimos com sucesso o makeState() de criar estados que não são de número ou de strings.

Como você acabou de ver, você pode especificar o que é permitido para os parâmetros de tipo de uma função genérica.

# Tipo Padrão

Pode ser irritante especificar tipos como `<number>` ou `<string>` toda vez que você chama `makeState()`.

Então, aqui está uma idéia: podemos dizer que `<number>` é o parâmetro de tipo padrão de makeState()?

Queremos fazer com que, se S não for especificado, ele seja definido como number por padrão.

```tsx
// Podemos dizer que <number> é 
// o tipo padrão de makeState()?

// Queremos que o seguinte seja equivalente
const numState1 = makeState()
const numState2 = makeState<number>()
```

Para que isso aconteça, podemos especificar o tipo padrão de `S` adicionando `= number` no final. É como definir valores padrão para parâmetros de função regulares, certo?

```tsx
// Setamos o tipo padrão de S para number
function makeState<
  S extends number | string = number
>()
```

Ao fazer isso, você pode criar um estado apenas de número sem especificar o tipo:

```tsx
// Não precisamos usar <number>
const numState = makeState()

numState.setState(1)
console.log(numState.getState())
```

# Recapitulação rápida: Iguais aos parâmetros de função regulares

Estamos em cerca de dois terços no caminho deste artigo. Antes de continuarmos, vamos fazer uma rápida recapitulação.

O que você deve se lembrar é que Generics são como parâmetros de função regulares. A diferença é que parâmetros de função regulares lidam com valores, mas Generics lidam com parâmetros de tipo. 

Exemplo 1: Por exemplo, aqui está uma função regular que aceita qualquer valor:

```tsx
// Declara uma função regular
function regularFunc(x: any) {
  // Você pode usar o valor x aqui
}

// x será 1
regularFunc(1)
```

Da mesma forma, você pode declarar uma função genérica com um parâmetro de tipo:

```tsx
// Declara uma função genérica
function genericFunc<T>() {
  // Você pode usar o tipo T aqui
}

// T será um "number"
genericFunc<number>()
```

Exemplo 2: Em funções regulares, você pode especificar o tipo de um parâmetro como este:

```tsx
// Declara que x será um número
function regularFunc(x: number)

// Sucesso
regularFunc(1)

// Error
regularFunc('foo')
```

Da mesma forma, você pode especificar o que é permitido para o parâmetro de tipo de uma função genérica:

```tsx
// Limita o tipo de T
function genericFunc<T extends number>()

// Sucesso
genericFunc<number>()

// Error
genericFunc<string>()
```

Exemplo 3: Em funções regulares, você pode especificar o valor padrão de um parâmetro como este:

```tsx
// Declara o valor padrão de x
function regularFunc(x = 2)

// x terá o valor 2 dentro da função
regularFunc()
```

Da mesma forma, você pode especificar o tipo padrão para uma função genérica:

```tsx
// Declara o tipo padrão de T
function genericFunc<T = number>()

// T será "number" dentro da função
genericFunc()
```

Os Generics não são assustadores. Eles são como parâmetros de função regulares, mas, em vez de valores, eles são tipos. Se você entendeu até aqui, você está pronto!

# Vamos falar sobre makePair()

Vamos dar uma olhada na nova função chamada `makePair()`. É semelhante ao `makeState()`, mas, em vez de armazenar um único valor, este armazena um par de valores como `{ first: ?, second: ? }`. No momento, ele suporta apenas números:

```tsx
function makePair() {
  // Guarda um par de valores
  let pair: { first: number; second: number }
  function getPair() {
    return pair
  }

  // Guarda x como "first" e y como "second"
  function setPair(x: number, y: number) {
    pair = {
      first: x,
      second: y
    }
  }
  return { getPair, setPair }
}
```

Vamos experimentar! O que é impresso no console quando você executa o código a seguir?

```tsx
const { getPair, setPair } = makePair()

setPair(1, 2)
console.log(getPair())

setPair(3, 4)
console.log(getPair())
```

Agora, assim como fizemos para `makeState()`, vamos transformar `makePair()` em uma função genérica.

# Tornando makePair uma função genérica

Abaixo está uma versão genérica de `makePair()`.

-  São necessários dois parâmetros de tipo, `F` e `S` (o "F" para "first" e "S" para "second").
-  O tipo de "first" será F
-  O tipo de "second" será S

```tsx
function makePair<F, S>() {
  let pair: { first: F; second: S }
  function getPair() {
    return pair
  }
  function setPair(x: F, y: S) {
    pair = {
      first: x,
      second: y
    }
  }
  return { getPair, setPair }
}
```

Aqui está um exemplo de uso. Ao chamar `makePair` com `<number, string>`, irá declarar `first` para ser `number` e o `second` para ser `string`.

```tsx
// Cria um par "number"/"string"
const { getPair, setPair } = makePair<
  number,
  string
>()

// Deve passar "number" e "string"
setPair(1, 'hello')
```

Para resumir, você pode criar uma função genérica que leva parâmetros de tipo múltiplo.

```tsx
// makeState() tem 1 parâmetro de tipo
function makeState<S>()

// makeState() tem 2 parâmetro de tipo
function makePair<F, S>()
```

Obviamente, você também pode usar a palavra-chave `extends` ou os tipos padrão como antes:

```tsx
function makePair<
  F extends number | string = number,
  S extends number | string = number
>()
```

Você pode até fazer com que o segundo tipo (S) seja relacionado ao primeiro tipo (F). Aqui está um exemplo:

```tsx
// O parâmetro "second"/"S" precisa ser
// "boolean" ou qualquer valor de "F"
function makePair<
  F extends number | string,
  S extends boolean | F
>()

// Esses irão funcionar
makePair<number, boolean>()
makePair<number, number>()
makePair<string, boolean>()
makePair<string, string>()

// Esse irá falhar pois o parâmetro "second"
// precisa ser "boolean" ou "number" mas
// o tipo "string" foi passado
makePair<number, string>()
```

# Interfaces Genéricas e Aliases de Tipo

Vamos voltar à nossa implementação anterior de `makePair()`. Agora, dê uma olhada no tipo de `pair`:

```tsx
function makePair<F, S>() {
  let pair: { first: F; second: S }
  // ...
}
```

Isso funciona, mas se quisermos, podemos refatorar `{ first: F, second: S }` em uma `interface` ou um `alias de tipo` para que possa ser reutilizado.

Vamos primeiro extrair o tipo de `pair` em uma `interface genérica`. Vou usar A e B como nomes de parâmetros de tipo para distingui-los dos parâmetros de tipo de `makePair()`.

```tsx
// Extraindo os tipos em uma
// interface genérica para
// podermos reutilizá-las.
interface Pair<A, B> {
  first: A
  second: B
}
``` 

Podemos então usar essa interface para declarar o tipo de `pair`.

```tsx
function makePair<F, S>() {
  // Passamos F para A e S para B
  let pair: Pair<F, S>
  // ...
}
```

Ao extrair para uma interface genérica (uma interface que aceita parâmetros de tipo), podemos reutilizá-la em outros lugares, se necessário.

Como alternativa, podemos extraí-lo em um alias de tipo genérico. Para tipos de objetos, os aliases de tipo são basicamente idênticos às interfaces, portanto você pode usar o que preferir.

```tsx
// Extraindo os tipos para um alias de tipo
// genérico. Nesse caso, eles são idênticos
// as interfaces.
type Pair<A, B> = {
  first: A
  second: B
}
```

Para resumir, você pode criar interfaces genéricas e aliases de tipo, assim como você pode criar funções genéricas.

**Curiosidade**

Para saber mais sobre interfaces vs aliases de tipo, [leia esta resposta no StackOverflow](https://stackoverflow.com/a/52682220/114157). A partir do TypeScript 3.7, [que adicionou um suporte para aliases de tipo recursivo](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html), os aliases de tipo podem abranger praticamente todos os casos de uso de interfaces.

# Classes Genéricas

A última coisa que iremos abordar são as classes genéricas. Primeiro, vamos revisitar o código para `makeState()`. Esta é a versão genérica que não usa `extends` ou parâmetros de tipo padrão.

```tsx
function makeState<S>() {
  let state: S
  function getState() {
    return state
  }
  function setState(x: S) {
    state = x
  }
  return { getState, setState }
}
```

Podemos transformar `makeState()` em uma classe genérica chamada `State` como abaixo. Parece semelhante a `makeState()`, certo?

```tsx
class State<S> {
  state: S
  getState() {
    return this.state
  }
  setState(x: S) {
    this.state = x
  }
}
```

Para usar isso, você só precisa passar um parâmetro de tipo na inicialização.

```tsx
// Passa o parâmetro de tipo na inicialização
const numState = new State<number>()
numState.setState(1)

// Imprime 1
console.log(numState.getState())
```

Para resumir, classes genéricas são como funções genéricas. Funções genéricas usam um parâmetro de tipo quando chamadas, mas classes genéricas usam um parâmetro de tipo quando são instanciadas.

**Nota:** Você precisa definir `"strictPropertyInitialization": false` na configuração do `tsconfig.json` para o código acima compilar.

# Créditos

- [TypeScript Generics for People Who Gave Up on Understanding Generics](https://ts.chibicode.com/generics), escrito originalmente por [Shu Uesugi](https://twitter.com/chibicode/).