DEV Community

Cover image for Typescript Avançado
Ivan Trindade
Ivan Trindade

Posted on

Typescript Avançado

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:

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' });

Enter fullscreen mode Exit fullscreen mode

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:

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',
});

Enter fullscreen mode Exit fullscreen mode

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:

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

type characterProps = keyof MovieCharacter;

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

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:

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');

Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

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:

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.
Enter fullscreen mode Exit fullscreen mode

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.

condition ? returnTypeIfTrue : returnTypeIfFalse;

Enter fullscreen mode Exit fullscreen mode

O mesmo conceito também existe no TypeScript:

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;

Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

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

Enter fullscreen mode Exit fullscreen mode

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:

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',
});

Enter fullscreen mode Exit fullscreen mode

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:

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 };

Enter fullscreen mode Exit fullscreen mode

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:

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';

Enter fullscreen mode Exit fullscreen mode

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.

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;

Enter fullscreen mode Exit fullscreen mode

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:

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;
}

*/

Enter fullscreen mode Exit fullscreen mode

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:

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;
}

 */

Enter fullscreen mode Exit fullscreen mode

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:

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;
}

*/

Enter fullscreen mode Exit fullscreen mode

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.

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;
}

 */

Enter fullscreen mode Exit fullscreen mode

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:

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;
}

*/

Enter fullscreen mode Exit fullscreen mode

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?

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;
}

*/

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)