Aprofundando em Types
Olá leitor, seja bem vindo a mais uma lição da jornada do desenvolvedor Typescript 😉
Hoje nós iremos nos aprofundar um pouco mais nos tipos de dados do Typescript, principalmente em tipos genéricos, e os possíveis operadores que podemos usar em conjunto com eles.
A criação de tipos em Typescript, pode parecer um tipo de processo bastante trabalhoso em primeira vista, mas garanto para você que vale a pena perder um pouco mais de tempo tipando suas variáveis, do que se deparar com possíveis bugs depois.
Sendo assim, partiu nos aprofundarmos ainda mais em Types
!
Recapitulando os Tipos Genéricos
Em lições anteriores, você aprendeu sobre os Tipos Genéricos, que nos permitem criar funções que aceitam mais de um tipo de dado específico.
Vamos ver novamente um exemplo?
function retornaNome<T>(nome: T): string{
return 'O Nome é: ' + nome;
}
console.log(retornaNome(99));
console.log(retornaNome("Micilini Roll"));
Vimos também que podemos reduzir os tipos de dados que são aceitos em nossas funções genéricas por meio das constraints:
function mostraProduto<T extends {nome: string}>(produto: T): string{
return 'O nome do produto é: ' + produto.nome;
}
const produto = {nome: 'Maquina de Lavar', preco: 399.90};
console.log(mostraProduto(produto));
No comando acima, nós estamos limitando de forma que nosso objeto tenha pelo menos uma propriedade chamada de nome
.
Se você quiser que mais de uma propriedade seja garantida, basta adicioná-la dentro da constraints
, da seguinte forma:
function mostraProduto<T extends {nome: string, preco: number}>(produto: T): string{
return 'O nome do produto é: ' + produto.nome + ', e seu preço é R$ ' + produto.preco;
}
const produto = {nome: 'Maquina de Lavar', preco: 399.90};
console.log(mostraProduto(produto));
Legal, não?
Usando Tipos Genéricos com Interfaces
Fazendo o uso dos tipos genéricos dentro de uma interface, faz com que a tipagem fique um pouco mais fraca, a tornando mais "genérica".
Vamos ver como funciona seu uso na prática:
interface Container<T> {
value: T; // O valor armazenado no contêiner, de tipo genérico T
name?: string; // Nome opcional do contêiner
}
No comando acima, nós temos uma interface chamada de Container
que recebe tipos genéricos, ou seja, a propriedade value
pode ser qualquer tipo de dado.
Diferente da propriedade name
, que além de ser opcional, armazena uma string
.
Vejamos agora, como usar essa interface
na prática:
const numberContainer: Container<number> = {
value: 42,
name: 'Answer to the Ultimate Question'
};
const stringContainer: Container<string> = {
value: 'Hello, TypeScript!',
name: 'Greeting'
};
const booleanContainer: Container<boolean> = {
value: true
};
No comando acima, nós criamos 3 tipos de variáveis que criam objetos seguindo a interface genérica que acabamos de criar.
No caso de uma interface genérica, nós ainda precisamos atribuir um tipo de dado quanto fazemos o uso dela:
Container<number>
Container<string>
Container<boolean>
Esse é a sintaxe padrão quando estamos declarando uma interface que possui um tipo genérico 🙂
Vamos criar agora, uma outra função que recebe a interface generica que nós criamos, de modo a processar seus dados:
function displayContainer<T>(container: Container<T>): void {
console.log(`Value: ${container.value}`);
if (container.name) {
console.log(`Name: ${container.name}`);
}
}
// Exibindo os contêineres
displayContainer(numberContainer);
displayContainer(stringContainer);
displayContainer(booleanContainer);
Nada além do que já vimos com o Typescript, não é mesmo?
Usando Types com Tipos Genéricos de Interface
Para nos aprofundarmos ainda mais no exemplo de tipos genéricos com Interfaces, gostaria que você desse uma olhada nesse comando abaixo:
interface MeuObjeto<T, U, X> {
nome: T,
idade: U,
rank: X,
status: boolean
}
type Pessoa = MeuObjeto<string, number, number>;
type Animal = MeuObjeto<number, boolean, boolean>;
const minhaPessoa: Pessoa = {nome: "Micilini", idade: 88, rank: 100, status: true};
const meuAnimal: Animal = {nome: 25, idade: true, rank: false, status: false};
No código acima usamos o Type
que referência nossa interface genérica (MeuObjeto
), e já seta de forma padrão os tipos de dados que a nossa interface genérica irá receber.
Explorando Types com Typescript
Agora, nós iremos explorar um pouco mais sobre os usos dos types
e seus operadores em Typescript, que são:
Type Parameters
KeyOf
TypeOf
Indexed Access
Conditional Types
- Template de
Literals Type
Vamos nessa? 😋
Type Parameters
Os Type Parameters
(Parâmetros de Tipo) permitem que você crie tipos genéricos que podem ser utilizados com diferentes tipos de dados.
Isso é útil para escrever funções, classes e interfaces que podem trabalhar com qualquer tipo sem perder a segurança de tipo.
Vamos ver alguns exemplos:
function identity<T>(arg: T): T {
return arg;
}
const num = identity(5); // num é do tipo number
const str = identity('hello'); // str é do tipo string
No exemplo acima, T
é um parâmetro de tipo que pode ser qualquer tipo, e a função identity
mantém o tipo do argumento passado.
Dessa forma nós conseguimos criar uma ligação entre o tipo genérico e a sua chave.
Com os Type Parameters
, nós podemos dizer que algum parâmetro de uma função é a chave de um objeto (que também é um parâmetro):
function pegaUmaChave<T, K extends keyof T>(obj: T, key: K): string{
return 'A chave ' + obj[key] + ' está presente dentro do objeto, e possui o valor ' + obj[key];
}
const pessoa = {
nome: 'Micilini Roll',
status: true,
}
console.log(pegaUmaChave(pessoa, 'nome'));
No comando acima estamos verificando se dentro do objeto pessoa
existe a chave nome
.
Para simular um erro vamos tentar executar esse comando abaixo:
console.log(pegaUmaChave(pessoa, 'roll'));
Olha a mensagem de erro:
Com o uso do Type Parameters
, o compilador não deixa que este código seja executado, pois ele já entende que a chave roll
não existe dentro do objeto
pessoa
.
KeyOf
Como vimos anteriormente, o KeyOf
é um comando usado para obter um tipo que é uma união de todas as chaves de um tipo dado.
Se tornando bastante útil quando você quer restringir valores ou tipos a chaves específicas de um objeto.
Vejamos um exemplo simples da sua utilização:
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
const key: PersonKeys = 'name'; // Válido
// const invalidKey: PersonKeys = 'address'; // Erro: Type '"address"' is not assignable to type 'PersonKeys'
No comando acima, PersonKeys
é um tipo que pode ser 'name'
ou 'age'
, que se relaciona com as chaves da interface Person
.
Lembrando que este tipo aceita dados do tipo objeto
, arrays
e object literals
.
Vejamos um outro exemplo em que fazemos o uso do Type
:
type Pessoa = {nome: string, idade: number, rank: number};
type P = keyof Pessoa;
function mostraNome(pessoaObj: Pessoa, key: P): void{
console.log(pessoaObj.nome);
}
const micilini: Pessoa = {
nome: "Micilini Roll",
idade: 30,
rank: 100
}
mostraNome(micilini, 'nome');
mostraNome(micilini, 'site');//Este comando vai gerar um erro, pois a propriedade 'site' não existe.
No comando acima, nós estamos criando uma função que verifica se as chaves 'nome'
e 'site'
existem dentro do objeto micilini
, que por sua vez, está conectado com a interface Pessoa
.
TypeOf
O tipo TypeOf
é usado para obter o tipo de uma variável ou de uma expressão em tempo de execução. Isso é útil quando você deseja criar um tipo que corresponda ao tipo de uma variável existente.
Vamos ver um exemplo simples da sua utilização:
const greeting = "Hello, world!";
type GreetingType = typeof greeting; // string
const message: GreetingType = "Hello, TypeScript!"; // Válido
// const wrongMessage: GreetingType = 123; // Erro: Type 'number' is not assignable to type 'string'
No caso do comando acima, GreetingType
é do tipo string
, que é o tipo da variável greeting
.
O que se torna bastante útil quando queremos criar uma variável com o mesmo tipo da outra.
Indexed Access
Os Indexed Access Types
permitem acessar o tipo de uma propriedade específica em um tipo ou interface. Isso é útil para criar tipos que dependem da estrutura de outros tipos.
Vejamos um exemplo:
interface Person {
name: string;
age: number;
}
// Tipo que representa o tipo da propriedade 'name' em Person
type NameType = Person['name']; // string
const name: NameType = 'Alice'; // Válido
// const invalidName: NameType = 42; // Erro: Type 'number' is not assignable to type 'string'
No exemplo acima, NameType
é o tipo da propriedade name
da interface Person
, que é string
.
Com os Indexed Access
, nós podemos criar um tipo baseado em uma chave de objeto, para que mais tarde, possamos reaproveitar o tipo dessa chave
para ela possa ser usada em outros locais (no caso de funções e classes).
Conditional Types
Com os Conditional Types
nós conseguimos criar tipos com base em uma condição, semelhante a um operador ternário. Eles são úteis para criar tipos que variam dependendo de outras condições de tipo.
Vejamos um exemplo:
type IsString<T> = T extends string ? 'Yes' : 'No';
type Test1 = IsString<string>; // 'Yes'
type Test2 = IsString<number>; // 'No'
No exemplo acima, IsString
verifica se o tipo T
é um string
. Se for, resulta em 'Yes'
, caso contrário, resulta em 'No'
.
Template Literal Types
Os Template Literal Types
permitem criar novos tipos combinando outros tipos usando a sintaxe de template strings
, semelhante ao que fazemos com as strings
no JavaScript.
Vejamos um exemplo:
type Color = 'red' | 'green' | 'blue';
type ColoredItem = `The color is ${Color}`;
// Exemplos válidos
const color1: ColoredItem = 'The color is red';
const color2: ColoredItem = 'The color is blue';
// Exemplos inválidos
// const invalidColor: ColoredItem = 'The color is yellow'; // Erro: Type '"The color is yellow"' is not assignable to type 'ColoredItem'
No caso do ColoredItem
, ele é um tipo que corresponde a strings
que seguem o padrão "The color is "
, seguido por um dos valores em Color
.
Arquivos da lição
Os arquivos desta lição podem ser encontrados no repositório do GitHub por meio deste link.