[ Conteúdo ] Inferência no TypeScript
introdução
Vamos falar um pouco sobre como funciona inferência no TypeScript? Pode, ou não pode ser novidade, mas é bem semelhante a forma que acontece no javaScript. Vou apenas pontuar pontos que estão presentes na documentação do TypeScript. Não vou me aprofundar em casos específicos, como isso interage com outros ambientes ou configurações. Então, sem mais delongas.
O que é inferência?
Uma inferência ocorre no TypeScript, quando não declaramos o tipo de uma variável de forma explícita. Exemplo:
let animeName = 'Sousou no Frieren';
let idx = 10;
let closed = true;
temos três variáveis com tipos de valores diferentes, porém, nenhuma dessas três variáveis estão explicitamente declarada com um determinado tipo, como faríamos normalmente desta forma:
let animeName: string = 'Sousou no Frieren';
let idx: number = 10;
let closed: boolean = true;
Ok, mas o que acontece no exemplo onde não declaramos o tipo? Bom, como já deve imaginar, o TypeScript vai inferir isso por debaixo dos panos. Ele vai analisar o valor que está dentro da variável, se for uma string
, automaticamente o TypeScript vai typa como uma variável string
, e o mesmo ocorre para as outras variáveis. Se houver um tipo number
, TypeScript vai inferir a variável como tipo Number
.
É semelhante ao que ocorre no javaScript, mas não é a mesma coisa!
Best common type
Agora vamos ver o que acontece, quando temos um determinado array com diversos valores diferentes, como este aqui:
let x = [0, 1, null];
Como o typeScript vai inferir isso? É mais simples do que se pensa! O TypeScript vai olhar para isso e vai considerar todos os elementos e seus tipos, e vai pegar ambos e inferir isso a variável. Então no caso, o tipo inferido seria este aqui:
let x = [0, 1, null]; // tipo inferido: number | null
TypeScript faz a união dos tipos e infere isso para a variável, assim dando duas possibilidades de possíveis valores!
Agora vamos para um exemplo onde todos os elementos são instâncias objetos instânciados, e possui uma estrutura comum.
let zoo = [new Rhino(), new Elephant(), new Snake()];
Quando você cria um array com vários objetos de diferentes classes, como Rhino, Elephant e Snake, o TypeScript tenta inferir o melhor tipo comum entre os elementos do array. No entanto, se não houver um tipo único que seja super tipo de todos os objetos no array, o TypeScript não poderá inferir o tipo Animal[]
para o array zoo. Isso acontece porque, embora cada uma dessas classes possa compartilhar uma estrutura comum (como se todas fossem subclasses de Animal), nenhum dos objetos é estritamente do tipo Animal.
Para resolver isso, você pode explicitamente anotar o tipo do array como Animal[].
Isso informa ao TypeScript que todos os objetos no array zoo são do tipo Animal, o que pode ser útil quando você quer garantir que o array contenha apenas objetos desse tipo.
Se não for fornecida uma anotação de tipo e o TypeScript não puder encontrar um tipo comum, ele inferirá um tipo de união para os elementos do array. No caso do array zoo, o tipo inferido seria (Rhino | Elephant | Snake)[]
, o que significa que cada elemento no array pode ser um Rhino, um Elephant, ou um Snake.
// Sem anotação de tipo explícita, o TypeScript não pode inferir um tipo único `Animal[]`
let zoo = [new Rhino(), new Elephant(), new Snake()]; // Inferido como (Rhino | Elephant | Snake)[]
// Com anotação de tipo explícita, informamos ao TypeScript que queremos um array de `Animal`
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Agora é do tipo Animal[]
Ok, ok. Entedemos agora como funciona inferência de objetos comuns em um Array, mas, quais são as vantagens disso? Vou listar elas agora:
Ter todos os objetos do tipo Animal[]
pode ser benéfico por várias razões:
Abstração: Ao tratar todos os animais como instâncias da classe Animal, você pode abstrair as diferenças entre os tipos específicos de animais (Rhino, Elephant, Snake)
. Isso permite que você escreva código que opera em um nível mais alto de abstração, trabalhando com animais genéricos em vez de ter que lidar com cada tipo específico de animal individualmente.
Tipos Polimórficos: Em programação orientada a objetos, um tipo polimórfico é um tipo que pode assumir muitas formas diferentes. Isso é particularmente útil quando você tem um array de objetos que compartilham uma interface comum, mas são instâncias de classes diferentes. Ao declarar o array como Animal[]
, você está dizendo ao TypeScript que o array pode conter qualquer tipo de objeto que seja uma instância da classe Animal.
Para acessar os valores dentro de Animal[],
você pode usar loops ou métodos de array como map, filter, etc. Como cada item no array é um objeto do tipo Animal, você pode acessar os métodos e propriedades definidos na classe Animal. Aqui está um exemplo:
for (let animal of zoo) {
console.log(animal.name); // Supondo que todos os animais têm uma propriedade 'name'
}
Embora Animal seja uma classe, Animal[]
é um array de objetos que são instâncias da classe Animal. Portanto, cada item no array zoo é um objeto Animal e você pode acessar suas propriedades e métodos como faria normalmente com um objeto.
Contextual Typing
Uma coisa muito comum no TypeScript é a tipage pelo o contexto. Isso é um mecanismo muito poderoso no TypeScript e vamos entrar de cabeça neste conceito agora.
Contextual Typing ocorre quando seu tipo é implícito com base na sua localização, como desta forma:
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button);
console.log(mouseEvent.kangaroo);
// Property 'kangaroo' does not exist on type 'MouseEvent'.
};
Aqui o typeScript consegue inferir o tipo de mouseEvent
corretamente e não reclama do método button
, pois esse método existe para este tipo. Já para o método kangaroo
...
Isso funciona pois windows
já tem onmousedown
declarado em seu tipo:
declare var window: Window & typeof globalThis;
interface Window extends GlobalEventHandlers {
// ...
}
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
// ...
}
A interface Window é então estendida pela interface GlobalEventHandlers, que define um conjunto de event handlers globais, como onmousedown, onkeyup, etc. Esses event handlers são usados para lidar com eventos de mouse e teclado que ocorrem na janela do navegador.
TypeScript é inteligente para inferir tipos de outros também:
window.onscroll = function (uiEvent) {
console.log(uiEvent.button);
// Property 'button' does not exist on type 'Event'.
};
O typeScript sabe que onscoll
é um UIevent, e não um mouseevent
. E como UIevents não contém métodos como button
, será lançado um typeError
Se este evento não estivesse em uma posição onde é favorável para o TypeScript inferir valores, então uiEvent
seria atribuído como any
const handler = function (uiEvent) {
console.log(uiEvent.button); // <- OK
};
Claro, se não estiver configurado em seu arquivo tsconfig.json
com: noImplicitAny
;
Também é possível declarar isso implicitamente:
window.onscroll = function (uiEvent: any) {
console.log(uiEvent.button); // <- Agora nenhum erro é lançado
};
No entanto, este código vai retornar um undefined
uma vez que onscroll
não tem o método button
.
Contextual Typing ocorre em retornos de funções, chamadas de argumentos de função, lado direito de valores atribuídos, type assertions, membros de objetos e arrays literais.
Conlusão
Gostaram do conteúdo? Quer complementar com algo? Cometi algum eventual erro? Comentem!