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' });
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',
});
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;
Entendi! Mas quando isso é útil? Também poderíamos digitar o characterProps
.
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:
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:
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:
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
.
condition ? returnTypeIfTrue : returnTypeIfFalse;
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;
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',
});
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',
});
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 };
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';
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;
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;
}
*/
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;
}
*/
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;
}
*/
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;
}
*/
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;
}
*/
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;
}
*/
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)