Typescript Avançado
Tags: typescript, tutorial, programming
Published: 2023-01-03 20:36:21

Neste artigo, abordaremos alguns dos maiores recursos avançados do Typescript. Ele oferece muitos recursos excelente, aqui está o resumo de alguns recursos avançados do Typescript que veremos a seguir:

- Tipos de união e interseção
- Keyof
- Typeof
- Tipos condicionais
- Tipos de utilidade
- Inferir tipo
- Tipos mapeados

Ao final desse artigo, você deve ter uma compreensão básica de cada um desses operadores e deve ser capaz de usá-los em seus projetos.

## Tipos de união e interseção

Typescript nos permite combinar vários tipos para criar um novo tipo. Essa abordagem é semelhante as expressões lógicas em JavaScript, onde podemos usar o operador lógico **OR** `||` ou o operador lógico **AND** `&&`, para criar novas verificações poderosas.

### Tipos de união

Um tipo de união, é semelhante á expressão OR do JavaScript. Ele permite que você use dois ou mais tipos (sindicatos), para formar um novo tipo que pode ser qualquer um desses tipos:

```jsx
function orderProduct(orderId: string | number) {
  console.log('Ordering product with id', orderId);
}

// 👍
orderProduct(1);

// 👍
orderProduct('123-abc');

// 👎 O argumento não pode ser atribuído à string | number
orderProduct({ name: 'foo' });

```

Digitamos o método `orderProduct` com um tipo de união. O TypeScript lançará um erro assim que chamarmos o método `orderProduct`, com qualquer coisa que não seja **string** ou **number**.

## Tipos de interseção

Um tipo de interseção, por outro lado, combina vários tipos em um. Este novo tipo tem todas as características dos tipos combinados:

```jsx
interface Person {
  name: string;
  firstname: string;
}

interface FootballPlayer {
  club: string;
}

function tranferPlayer(player: Person & FootballPlayer) {}

// 👍
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio',
  club: 'PSG',
});

// 👎 O argumento não pode ser atribuído a Person & FootballPlayer
transferPlayer({
  name: 'Ramos',
  firstname: 'Sergio',
});

```

O método `transferPlayer` aceita um tipo que contém todos os recursos de `Person` e `FootballPlayer`. Somente um objeto contendo o `name`, `firstName` e a propriedade `club`, é válido.

## Keyof

Agora que sabemos o tipo de união, vamos dar uma olhada no operador `Keyof`. O operador `keyof` pega as chaves de uma interface ou objeto e produz um tipo de união:

```jsx
interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

type characterProps = keyof MovieCharacter;

```

Entendi! Mas quando isso é útil? Também poderíamos digitar o `characterProps`.

```jsx
type characterProps = 'firstname' | 'name' | 'movie';

```

Sim, poderíamos. Keyof torna nosso código mais robusto e sempre mantrém nossos tipos atualizados. Vamos explorar isso com o exemplo a seguir:

```jsx
interface PizzaMenu {
  starter: string;
  pizza: string;
  beverage: string;
  dessert: string;
}

const simpleMenu: PizzaMenu = {
  starter: 'Salad',
  pizza: 'Pepperoni',
  beverage: 'Coke',
  dessert: 'Vanilla ice cream',
};

function adjustMenu(
  menu: PizzaMenu,
  menuEntry: keyof PizzaMenu,
  change: string,
) {
  menu[menuEntry] = change;
}

// 👍
adjustMenu(simpleMenu, 'pizza', 'Hawaii');
// 👍
adjustMenu(simpleMenu, 'beverage', 'Beer');

// 👎 Type - 'bevereger' is not assignable
adjustMenu(simpleMenu, 'bevereger', 'Beer');
// 👎 Wrong property - 'coffee' is not assignable
adjustMenu(simpleMenu, 'coffee', 'Beer');

```

A função `adjustMenu` permite alterar um menu. Por exemplo, imagine que você gosta de cerveja, mas `menuSimples` prefere beber cerveja com Coca-Cola. Nesse caso, chamamos a função `ajustMenu` com o menu, `menuEntry` e o `change`, no nosso caso, uma Beer.

A parte interessante da função, é que o `menuEntry` é digitado com o operador `keyof`. O bom aqui, é que nosso código é muito robusto. Se refatorarmos a interface `PizzaMenu`, não precisamos mexer na função `adjustMenu`. Está sempre atualizado com as keys do `PizzaMenu`.

## Typeof

`Typeof` permite extrair um tipo de um valor. Ele pode ser usado em um contexto de tipo, para se referir ao tipo de uma variável:

```jsx
let firstname = 'Frodo';
let name: typeof firstname;

```

Claro, isso não faz muito sentido em um cenário tão simples. Mas vamos ver um exemplo mais sofisticado. Neste exemplo, usamos `typeof` em combinação com `ReturnType`, para extrair informações de digitação de um tipo de retorno de funções:

```jsx
function getCharacter() {
  return {
    firstname: 'Frodo',
    name: 'Baggins',
  };
}

type Character = ReturnType<typeof getCharacter>;

/*

equal to 

type Character = {
  firstname: string;
  name: string;
}

*/

No exemplo acima, criamos um novo tipo com base no tipo de retorno da função `getCharacter`. O mesmo aqui, se refatorarmos o tipo de retorno dessa função, nosso tipo `Character` estará atualizado.
```

## Tipos condicionais

O operador condicional ternário, é um operador muito conhecido em JavaScript. O operador ternário leva três operandos. Uma condição, um tipo de retorno se a condição for `true` e um tipo de retorno se for `false`.

```jsx
condition ? returnTypeIfTrue : returnTypeIfFalse;

```

O mesmo conceito também existe no TypeScript:

```jsx
interface StringId {
  id: string;
}

interface NumberId {
  id: number;
}

type Id<T> = T extends string ? StringId : NumberId;

let idOne: Id<string>;
// equal to let idOne: StringId;

let idTwo: Id<number>;
// equal to let idTwo: NumberId;

```

Neste exemplo, usamos o tipo `Id` para gerar um tipo baseado em um uma `string`. Se `T` extends `string`, retornamos o tipo `StringId`. Se passarmos um `number`, retornamos o tipo `NumberId`.

## Tipos de utilidade

Os tipos utilitários são ferramentas auxiliares para facilitar a transformação de tipos comuns. Typescript oferece muitos tipos de utilitários. Muitos para cobrir nesse post, abaixo você pode encontrar uma seleção dos que eu mais uso:

### Partial

O tipo de utilitário Partial permite transformar uma interface em uma nova interface onde todas as propriedades são opcionais.

```jsx
interface MovieCharacter {
  firstname: string;
  name: string;
  movie: string;
}

function registerCharacter(character: Partial<MovieCharacter>) {}

// 👍
registerCharacter({
  firstname: 'Frodo',
});

// 👍
registerCharacter({
  firstname: 'Frodo',
  name: 'Baggins',
});

```

`MovieCharacter` requer um `firstname`, `name` e um `movie`. No entanto, a assinatura da função `registerPerson` usa o utilitário `Partial` para criar um novo tipo com opcional `firstname`, `name` opcional e `movie` opcional.

### Required

`Required` faz o contrário de `Partial`. Ele pega uma interface existente com propriedades opcionais e a transforma em um tipo onde todas as propriedades são necessárias:

```jsx
interface MovieCharacter {
  firstname?: string;
  name?: string;
  movie?: string;
}

function hireActor(character: Required<MovieCharacter>) {}

// 👍
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
  movie: 'The Lord of the Rings',
});

// 👎
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
});

```

Neste exemplo, as propriedades de MovieCharacter eram opcionais. Ao usar `Required`, transformamos em um tipo em que todas as propriedades são necessárias. Portanto, somente objetos contendo as propriedades `firstname`, `name` e `movie` são permitidos.

### Extract

Extract permite extrair informações de digitação de um tipo. Extract aceita dois Parâmetros, primeiro a Interface e depois o tipo que deve ser extraído:

```jsx
type MovieCharacters =
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };

type hpCharacters = Extract<MovieCharacters, string>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

type hpCharacters = Extract<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = {firstname: string; name: string };

```

`Extract<MovieCharacters, string>` cria um tipo de união com `hpCharacters`, que consiste apenas em strings. `Extract<MovieCharacters, {firstname: string}>` por outro lado, extrai todos os tipos de objetos que contêm um tipo `firstname: string.`

### Exclude 

Excluir faz o oposto de extrair. Ele permite que você gere um novo tipo, excluindo um tipo:

```jsx
type MovieCharacters =
  | 'Harry Potter'
  | 'Tom Riddle'
  | { firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, string>;
// equal to type hpCharacters = {firstname: string; name: string };

type hpCharacters = Exclude<MovieCharacters, { firstname: string }>;
// equal to type hpCharacters = 'Harry Potter' | 'Tom Riddle';

```

Primeiro, geramos um novo tipo que exclui todas as strings. Em seguida, geramos um tipo que exclui todos os tipos de objeto contendo `firstname: string`.

### Inferir tipo

`infer` permite criar um novo tipo. É semelhante a criar uma variável em Javascript com a palavra-chave `var`, `let` ou `const`.

```jsx
type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T;

type foo = flattenArrayType<string[]>;
// equal to type foo = string;

type foo = flattenArrayType<number[]>;
// equal to type foo = number;

type foo = flattenArrayType<number>;
// equal to type foo = number;

```

Uau, `getArrayType` parece bem complicado. Mas, na verdade, não é. Vamos passar por isso.

`T` extends `Array<infer ArrayType>` verifica se `T` estende um Array. Além disso, usamos a palavra-chave `infer` para obter o tipo de array. Pense nisso como armazenar o tipo em uma variável.

Em seguida, usamos o tipo condicional para retornar o `ArrayType` se `T` estender Array. Se não, retornamos `T`.

## Tipos mapeados

Tipos mapeados são uma ótima maneira de transformar tipos existentes em novos tipos. Por isso o termo mapa. Os tipos mapeados são atraentes e nos permitem criar tipos de utilitário personalizados:

```jsx
interface Character {
  playInFantasyMovie: () => void;
  playInActionMovie: () => void;
}

type toFlags<Type> = { [Property in keyof Type]: boolean };

type characterFeatures = toFlags<Character>;

/*

equal to 

type characterFeatures = {
  playInFantasyMovie: boolean;
  playInActionMovie: boolean;
}

*/

```

Criamos o tipo auxiliar `toFlags`, que recebe um tipo e mapeia todas as propriedades para serem do tipo de retorno `boolean`.

Muito legal. Mas fica ainda mais poderoso. Podemos adicionar ou remover o modificador `?` ou `readonly`, prefixando-os com um simples `+` ou `-`.

Vamos dar uma olhada em um exemplo onde criamos um tipo de utilitário `mutable`:

```jsx
type mutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = mutable<Character>;
/*

equal to

type mutableCharacter = {
  firstname: string;
  name: string;
}

 */

```

Cada propriedade do tipo `Character`, é somente leitura. Nossa interface `mutable` remove a propriedade `readonly`, porque a prefixamos com `-`.

O mesmo funciona na outra direção. Se adicionarmos um `+`, podemos criar um tipo auxiliar que pega uma interface e a transforma em uma interface onde cada propriedade é opcional:

```jsx
type optional<Type> = {
  [Property in keyof Type]+?: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type mutableCharacter = optional<Character>;

/* 

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

*/

```

Claro, essas duas abordagens também podem ser combinadas. Observe o próximo exemplo em que o tipo `optionalAndMutable` remove a propriedade `readonly` e adiciona um `?` que torna cada propriedade opcional.

```jsx
type optionalAndMutable<Type> = {
  -readonly [Property in keyof Type]+?: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = optionalAndMutable<Character>;

/*

equal to

type mutableCharacter = {
  firstname?: string;
  name?: string;
}

 */

```

Fica ainda mais poderoso. Vamos verificar o exemplo a seguir, onde criamos tipo utilitário, que transforma um tipo existente em um tipo de setters:

```jsx
type setters<Type> = {
  [Property in keyof Type as `set${Capitalize<
    string & Property
  >}`]: () => Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = setters<Character>;

/*

equal to

type character = {
  setFirstname: () => string;
  setName: () => string;
}

*/

```

Não há limitações. Podemos até reaproveitar tudo o que vimos até agora. Que tal um tipo mapeado que usa o tipo utilitário `Exclude`?

```jsx
type nameOnly<Type> = {
  [Property in keyof Type as Exclude<Property, 'firstname'>]: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type character = nameOnly<Character>;

/*

equal to 

type character = {
  name: string;
}

*/

```

## Conclusão

É isso. O TypeScript é incrível e oferece ainda mais recursos. Uma vez dominados, os conceitos descritos neste artigo são muito poderosos e podem tornar seu código mais robusto e, portanto, mais fácil de refatorar.