Olá Habitantes! Apresentamos outra novidade
" Professional TypeScript. Development of scalable JavaScript-applications " para a gráfica . Neste livro, os programadores que já são intermediários em JavaScript aprenderão como dominar o TypeScript. Você verá como o TypeScript pode ajudá-lo a dimensionar seu código 10x melhor e tornar a programação divertida novamente.
Abaixo está um trecho de um capítulo do livro "Tipos avançados".
Tipos avançados
O sistema de tipo TypeScript mundialmente conhecido surpreende até os programadores de Haskell com seus recursos. Como você já sabe, não é apenas expressivo, mas também fácil de usar: as restrições de tipo e os relacionamentos nele são concisos, compreensíveis e, na maioria dos casos, deduzidos automaticamente.
A modelagem de elementos dinâmicos de JavaScript, como protótipos, limites, sobrecargas de funções e objetos em constante mudança, requer um sistema de tipos e um sistema de tipos tão rico quanto o Batman levaria a bordo.
Iniciarei este capítulo com um mergulho profundo nos tópicos de subtipagem, compatibilidade, variância, variável aleatória e extensão. Em seguida, expandirei os detalhes da verificação de tipo baseada em fluxo, incluindo refinamento e totalidade. A seguir, demonstrarei alguns recursos de programação avançados no nível de tipo: conexão e mapeamento de tipos de objeto, uso de tipos condicionais, definição de proteções de tipo e soluções de fallback como asserções de tipo e asserções de atribuição explícitas. Finalmente, apresentarei alguns padrões avançados para melhorar a segurança de tipo: padronização de objeto companheiro, aprimoramentos de interface para tuplas, imitação de tipos nominais e extensão de protótipo segura.
Relacionamentos entre os tipos
Vamos examinar mais de perto os relacionamentos no TypeScript.
Subtipos e supertipos
Já tocamos na compatibilidade na seção Sobre Tipos na p. 34, então vamos mergulhar direto neste tópico, começando com a definição do subtipo.

Retorne à fig. 3.1 e veja as associações de subtipos integradas do TypeScript.

- Array é um subtipo de um objeto.
- Uma tupla é um subtipo de uma matriz.
- Tudo é um subtipo de qualquer.
- nunca é um subtipo de tudo.
- A classe Bird, que estende a classe Animal, é um subtipo da classe Animal.
De acordo com a definição que acabei de dar para um subtipo, isso significa que:
- Sempre que precisar de um objeto, você pode usar uma matriz.
- Sempre que um array é necessário, uma tupla pode ser usada.
- Onde quer que você precise, você pode usar um objeto.
- Você nunca pode usar em todos os lugares.
- Onde quer que você precise de um Animal, você pode usar Bird.
Um supertipo é o oposto de um subtipo.
SUPERTIPO
Se você tem dois tipos, A e B, e B é um supertipo de A, então você pode usar A com segurança sempre que B for necessário (Figura 6.2).

E novamente, com base no diagrama da Fig. 3.1:
- Array é um supertipo de tupla.
- O objeto é um supertipo de array.
- Qualquer um é um supertipo de tudo.
- Never não é supertipo de ninguém.
- Animal é um supertipo de pássaro.
É exatamente o oposto de subtipos e nada mais.
Variação
Para a maioria dos tipos, é fácil entender se um certo tipo A é um subtipo de B. Para tipos simples como número, string, etc., você pode consultar o diagrama na Fig. 3.1 ou determinar de forma independente que o número contido no número do sindicato | string é um subtipo desta união.
Mas existem tipos mais complexos, como os genéricos. Considere estas questões:
- Quando Array <A> é um subtipo de Array <B>?
- Quando o Formulário A é um subtipo do Formulário B?
- Quando a função (a: A) => B é um subtipo de função (c: C) => D?
Regras de subtipagem para tipos que contêm outros tipos (ou seja, tendo parâmetros de tipo como Array <A>, formulários com campos como {a: número} ou funções como (a: A) => B) são mais difíceis de compreender, porque não são consistente em diferentes linguagens de programação.
Para tornar as regras a seguir mais fáceis de ler, apresentarei alguns elementos de sintaxe que não funcionam no TypeScript (não se preocupe, não é matemático):
- A <: B significa "A é um subtipo do mesmo tipo B";
- A>: B significa "A é um supertipo do mesmo tipo B".
Variação de forma e matriz
Para entender por que as linguagens não concordam com as regras de subtipagem de tipos complexos, um exemplo com um formulário que descreve um usuário em um aplicativo ajudará. Nós o representamos por meio de alguns tipos:
// , .
type ExistingUser = {
id: number
name: string
}
// , .
type NewUser = {
name: string
}
Suponha que um estagiário em sua empresa tenha a tarefa de escrever um código para excluir um usuário. Ele começa com o seguinte:
function deleteUser(user: {id?: number, name: string}) {
delete user.id
}
let existingUser: ExistingUser = {
id: 123456,
name: 'Ima User'
}
deleteUser(existingUser)
deleteUser recebe um objeto do tipo {id?: número, nome: string} e passa um usuário existente do tipo {id: número, nome: string} para ele. Observe que o tipo de id da propriedade (número) é um subtipo do tipo esperado (número | indefinido). Portanto, todo o objeto {id: número, nome: string} é um subtipo de {id?: Número, nome: string}, portanto, o TypeScript permite isso.
Você vê algum problema de segurança? Há um: depois de passar ExistingUser para deleteUser, o TypeScript não sabe que o ID do usuário foi excluído, portanto, se você ler existingUser.id após excluir deleteUser (existingUser), o TypeScript ainda assumirá que existingUser.id é do tipo number.
Obviamente, usar um tipo de objeto onde seu supertipo é esperado não é seguro. Então, por que o TypeScript permite isso? O resultado final é que não foi feito para ser totalmente seguro. Seu sistema de tipos busca detectar erros reais e torná-los visíveis para programadores de todos os níveis. Como as atualizações destrutivas (como a exclusão de uma propriedade) são relativamente raras na prática, o TypeScript é relaxado e permite que você atribua um objeto onde seu supertipo é esperado.
E quanto ao caso oposto: é possível atribuir um objeto onde seu subtipo é esperado?
Vamos adicionar um novo tipo para o usuário antigo e, em seguida, remover o usuário com esse tipo (imagine adicionar tipos ao código que seu colega escreveu):
type LegacyUser = {
id?: number | string
name: string
}
let legacyUser: LegacyUser = {
id: '793331',
name: 'Xin Yang'
}
deleteUser(legacyUser) // TS2345: a 'LegacyUser'
//
// '{id?: number |undefined, name: string}'.
// 'string' 'number |
// undefined'.
Quando você envia um formulário com uma propriedade cujo tipo é um supertipo do tipo esperado, o TypeScript jura. Isso ocorre porque id é uma string | número | undefined e deleteUser tratam apenas do caso em que id é um número | Indefinido.
Enquanto espera um formulário, você pode passar um tipo com tipos de propriedade que são <: dos tipos esperados, mas não pode passar um formulário sem tipos de propriedade que são supertipos de seus tipos esperados. Quando falamos sobre tipos, dizemos: "Formulários TypeScript (objetos e classes) são covariantes nos tipos de suas propriedades." Ou seja, para que o objeto A seja atribuído ao objeto B, cada uma de suas propriedades deve ser <: a propriedade correspondente em B.
Covariância é um dos quatro tipos de variância:
Invariância
Especificamente necessária T.
Covariância
Necessário <: T.
Contravariância
necessária>: T.
A bivariância será
adequada para <: T ou>: T.
No TypeScript, cada tipo complexo é covariante em seus membros - objetos, classes, matrizes e tipos de retorno de função - com uma exceção: os tipos de parâmetro de função são contravariantes.
. , . ( ). , Scala, Kotlin Flow, , .
TypeScript : , , , (, id deleteUser, , , ).
Variação da função
Vamos considerar alguns exemplos.
A função A é um subtipo da função B se A tiver a mesma ou menos aridade (número de parâmetros) que B, e:
- O tipo this, pertencente a A, é indefinido ou>: do tipo this, pertencente a B.
- Cada um dos parâmetros A>: o parâmetro correspondente em B.
- Tipo de retorno A <: tipo de retorno B.
Observe que para que a função A seja um subtipo da função B, seu tipo e parâmetros devem ser>: contrapartes em B, enquanto seu tipo de retorno deve ser <:. Por que a condição se reverte? Por que a condição <: simples não funciona para cada componente (do tipo this, tipos de parâmetro e tipo de retorno), como é o caso com objetos, matrizes, uniões, etc.?
Vamos começar definindo três tipos (em vez de classe, você pode usar outros tipos, onde A: <B <: C):
class Animal {}
class Bird extends Animal {
chirp() {}
}
class Crow extends Bird {
caw() {}
}
Então Crow <: Bird <: Animal.
Vamos definir uma função que pega Bird e faz tweet:
function chirp(bird: Bird): Bird {
bird.chirp()
return bird
}
Por enquanto, tudo bem. O que o TypeScript permite que você canalize para chirp?
chirp(new Animal) // TS2345: 'Animal'
chirp(new Bird) // 'Bird'.
chirp(new Crow)
Uma instância de Bird (como um parâmetro de chirp do tipo bird) ou uma instância de Crow (como um subtipo de Bird). A passagem de subtipo funciona conforme o esperado.
Vamos criar uma nova função. Desta vez, seu parâmetro será uma função:
function clone(f: (b: Bird) => Bird): void {
// ...
}
clone requer uma função f que recebe Bird e retorna Bird. Que tipos de funções podem ser passados para f com segurança? Obviamente, a função que recebe e retorna Bird:
function birdToBird(b: Bird): Bird {
// ...
}
clone(birdToBird) // OK
Que tal uma função que pega um pássaro, mas retorna um corvo ou animal?
function birdToCrow(d: Bird): Crow {
// ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
// ...
}
clone(birdToAnimal) // TS2345: '(d: Bird) =>
// Animal'
// '(b: Bird) => Bird'. 'Animal'
// 'Bird'.
birdToCrow funciona conforme o esperado, mas birdToAnimal gera um erro. Por quê? Imagine uma implementação de clone assim:
function clone(f: (b: Bird) => Bird): void {
let parent = new Bird
let babyBird = f(parent)
babyBird.chirp()
}
Ao passar a função f para clone, que retorna Animal, não podemos chamar .chirp nela. Portanto, o TypeScript deve se certificar de que a função que passamos retorna pelo menos Bird.
Quando dizemos que as funções são covariantes em seus tipos de retorno, isso significa que uma função pode ser um subtipo de outra função somente se seu tipo de retorno for <: o tipo de retorno dessa função.
Ok, e quanto aos tipos de parâmetro?
function animalToBird(a: Animal): Bird {
// ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
// ...
}
clone(crowToBird) // TS2345: '(c: Crow) =>
// Bird' '
// (b: Bird) => Bird'.
Para que uma função seja compatível com outra função, todos os seus tipos de parâmetro (incluindo este) devem ser>: seus parâmetros correspondentes na outra função. Para entender por que, pense em como o usuário pode implementar o crowToBird antes de passá-lo para o clone.
function crowToBird(c: Crow): Bird {
c.caw()
return new Bird
}
TSC-: STRICTFUNCTIONTYPES
- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.
{«strict»: true}, .
Agora, se o clone chamar crowToBird com new Bird, obteremos uma exceção, pois .caw é definido em todos os Crow, mas não em todos os Birds.
Isso significa que as funções são contravariantes em seus parâmetros e neste tipo. Ou seja, uma função pode ser um subtipo de outra função apenas se cada um de seus parâmetros e tipo this for>: seus parâmetros correspondentes na outra função.
Felizmente, essas regras não precisam ser memorizadas. Apenas lembre-se deles quando o editor produzir um sublinhado vermelho quando você passar uma função digitada incorretamente em algum lugar.
Compatibilidade
Relacionamentos de subtipos e supertipos são um conceito-chave em qualquer linguagem de tipo estático. Eles também são importantes para entender como a compatibilidade funciona (lembre-se de que a compatibilidade se refere às regras do TypeScript que regem o uso do tipo A onde o tipo B é necessário).
Quando o TypeScript precisa responder à pergunta "O tipo A é compatível com o tipo B?", Segue regras simples. Para tipos não enum - como matrizes, booleanos, números, objetos, funções, classes, instâncias de classe e strings, incluindo tipos literais - A é compatível com B se uma das condições for verdadeira.
- A <: B.
- A é qualquer.
A regra 1 é apenas uma definição de subtipo: se A for um subtipo de B, então sempre que B for necessário, você pode usar A. A
regra 2 é uma exceção à regra 1 para facilitar a interação com o código JavaScript.
Para tipos de enumeração criados pelas palavras-chave enum ou const enum, o tipo A é compatível com a enumeração B se uma das condições for verdadeira.
- A é membro da enumeração B.
- B tem pelo menos um membro do tipo número e A é número.
A regra 1 é exatamente a mesma que para tipos simples (se A é um membro de B, então A é do tipo B e dizemos B <: B).
A regra 2 é necessária para a conveniência de trabalhar com enumerações, que comprometem seriamente a segurança do TypeScript (consulte a subseção “Enum” na página 60) e recomendo evitá-las.
Expansão de tipo A expansão de tipo
é a chave para entender como funciona a inferência de tipo. O TypeScript é tolerante na execução e tem mais probabilidade de errar ao deduzir um tipo mais geral do que o mais específico possível. Isso tornará sua vida mais fácil e reduzirá o tempo que leva para lidar com as notas do verificador de tipos.
No Capítulo 3, você já viu vários exemplos de expansão de tipo. Considere outros.
Quando você declara uma variável como mutável (com let ou var), seu tipo se expande do tipo de valor de seu literal para o tipo base ao qual o literal pertence:
let a = 'x' // string
let b = 3 // number
var c = true // boolean
const d = {x: 3} // {x: number}
enum E {X, Y, Z}
let e = E.X // E
Isso não se aplica a declarações imutáveis:
const a = 'x' // 'x'
const b = 3 // 3
const c = true // true
enum E {X, Y, Z}
const e = E.X // E.X
Você pode usar a anotação de tipo explícito para evitar que ele se expanda:
let a: 'x' = 'x' // 'x'
let b: 3 = 3 // 3
var c: true = true // true
const d: {x: 3} = {x: 3} // {x: 3}
Quando você reatribui um tipo não estendido com let ou var, o TypeScript o estende para você. Para evitar isso, adicione uma anotação de tipo explícito à declaração original:
const a = 'x' // 'x'
let b = a // string
const c: 'x' = 'x' // 'x'
let d = c // 'x'
Variáveis inicializadas como nulas ou indefinidas se expandem para qualquer:
let a = null // any
a = 3 // any
a = 'b' // any
Mas, quando uma variável, inicializada como nula ou indefinida, sai do escopo em que foi declarada, o TypeScript atribui a ela um tipo específico:
function x() {
let a = null // any
a = 3 // any
a = 'b' // any
return a
}
x() // string
O
tipo const O tipo const ajuda a evitar a extensão da declaração de tipo. Use-o como uma declaração de tipo (consulte a subseção "Aprovações de tipo" na página 185):
let a = {x: 3} // {x: number}
let b: {x: 3} // {x: 3}
let c = {x: 3} as const // {readonly x: 3}
const elimina a expansão de tipo e marca recursivamente seus membros como somente leitura, mesmo em estruturas de dados profundamente aninhadas:
let d = [1, {x: 2}] // (number | {x: number})[]
let e = [1, {x: 2}] as const // readonly [1, {readonly x: 2}]
Use as const quando quiser que o TypeScript deduza o mais estreito possível.
Verificando propriedades extras
A expansão de tipo também entra em ação quando o TypeScript verifica se um tipo de objeto é compatível com outro tipo de objeto.
Os tipos de objetos são covariantes em seus membros (consulte a subseção “Variação de forma e matriz” na página 148). Mas, se o TypeScript seguir esta regra sem verificações adicionais, podem surgir problemas.
Por exemplo, considere um objeto Options que você pode passar para uma classe para personalizá-lo:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
O que acontece agora se você errar na opção?
new API({
baseURL: 'https://api.mysite.com',
tierr: 'prod' // TS2345: '{tierr: string}'
}) // 'Options'.
//
// , 'tierr'
// 'Options'. 'tier'?
Esse é um bug comum do JavaScript e é bom que o TypeScript o ajude a detectá-lo. Mas se os tipos de objetos são covariantes em seus membros, como o TypeScript os intercepta?
Em outras palavras:
- Esperávamos o tipo {baseURL: string, cacheSize?: Número, camada?: 'Prod' | 'dev'}.
- Passamos o tipo {baseURL: string, tierr: string}.
- O tipo passado é um subtipo do tipo esperado, mas o TypeScript sabia relatar um erro.
Ao verificar as propriedades extras , quando você tenta atribuir um novo tipo de objeto literal T a outro tipo, U e T tem propriedades que U não possui, o TypeScript relata um erro.
O novo tipo de literal de objeto é o tipo que o TypeScript inferiu de um literal de objeto. Se este literal de objeto usa uma declaração de tipo (consulte a subseção “Asserções de tipo” na página 185) ou é atribuído a uma variável, o novo tipo é expandido para o tipo de objeto regular e sua novidade é perdida.
Vamos tentar tornar esta definição mais ampla:
type Options = {
baseURL: string
cacheSize?: number
tier?: 'prod' | 'dev'
}
class API {
constructor(private options: Options) {}
}
new API({ ❶
baseURL: 'https://api.mysite.com',
tier: 'prod'
})
new API({ ❷
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2345: '{baseURL:
}) // string; badTier: string}'
// 'Options'.
new API({ ❸
baseURL: 'https://api.mysite.com',
badTier: 'prod'
} as Options)
let badOptions = { ❹
baseURL: 'https://api.mysite.com',
badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
baseURL: 'https://api.mysite.com',
badTier: 'prod' // TS2322: '{baseURL: string;
} // badTier: string}'
// 'Options'.
new API(options)
❶ Instancie a API com baseURL e uma das duas propriedades opcionais: camada. Tudo está funcionando.
❷ Escrevemos por engano tier como badTier. O objeto de opções que passamos para a nova API é novo (seu tipo é inferido, é incompatível com a variável e não digitamos asserções para ele), portanto, ao verificar propriedades desnecessárias, o TypeScript detecta uma propriedade badTier extra (que é definida no objeto de opções, mas não em opções de tipo).
❸ Faça uma declaração de que o objeto de opções inválidas é do tipo Options. O TypeScript não o vê mais como novo e conclui a verificação das propriedades extras de que não há erros. A sintaxe as T é descrita na seção "Asserções de tipo" na p. 185
❹ Atribuição do objeto options à variável badOptions. O TypeScript não o percebe mais como novo e, após verificar as propriedades desnecessárias, conclui que não há erros.
❺ Quando digitamos opções explicitamente como Opções, o objeto que atribuímos a opções é novo, então o TypeScript verifica se há propriedades extras e encontra um bug. Observe que, neste caso, a verificação de propriedades extras não é feita quando passamos opções para a nova API, mas sim quando tentamos atribuir um objeto de opções à variável de opções.
Você não precisa memorizar essas regras. Esta é apenas uma heurística interna do TypeScript para detectar o máximo de bugs possível. Basta mantê-los em mente se de repente você se perguntar como o TypeScript descobriu um bug que nem mesmo Ivan - um veterano da sua empresa e também um censor de código profissional - não percebeu.
O refinamento do
TypeScript realiza a execução simbólica da inferência de tipo. O verificador de tipo usa instruções de fluxo de comando (como if,?, || e switch) junto com consultas de tipo (como typeof, instanceof e in), qualificando os tipos conforme um programador lê o código. No entanto, esse recurso útil é compatível com poucos idiomas.
Imagine que você desenvolveu uma API para definir regras CSS em TypeScript e seu colega deseja usá-la para definir a largura de um elemento HTML. Ele passa a largura que você deseja analisar e verificar mais tarde.
Primeiro, vamos implementar uma função para analisar uma string CSS em valor e unidade:
//
// , CSS
type Unit = 'cm' | 'px' | '%'
//
let units: Unit[] = ['cm', 'px', '%']
// . . null,
function parseUnit(value: string): Unit | null {
for (let i = 0; i < units.length; i++) {
if (value.endsWith(units[i])) {
return units[i]
}
}
return null
}
Em seguida, usamos parseUnit para analisar a largura fornecida pelo usuário. largura pode ser um número (possivelmente em pixels) ou uma string com as unidades anexadas, ou nula ou indefinida.
Neste exemplo, usamos a qualificação de tipo várias vezes:
type Width = {
unit: Unit,
value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
// width — null undefined, .
if (width == null) { ❶
return null
}
// width — number, .
if (typeof width === 'number') { ❷
return {unit: 'px', value: width}
}
// width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
// null.
return null ❹
}
❶ O TypeScript é capaz de entender que a igualdade frouxa do JavaScript em relação ao nulo retornará verdadeiro para nulo e indefinido. Ele também sabe que se a verificação passar, faremos uma devolução, e se não fizermos uma devolução, a verificação falhou e a partir desse momento, o tipo de largura é número | string (não pode mais ser nula ou indefinida). Dizemos que o tipo foi refinado a partir do número | string | null | indefinido em número | corda.
❷ O tipo de verificação pede um valor em tempo de execução para ver seu tipo. O TypeScript também tira vantagem do typeof em tempo de compilação: na ramificação if em que o teste passa, o TypeScript sabe que a largura é um número. Caso contrário (se este ramo retornar) a largura deve ser string - o único tipo restante.
❸ Como parseUnit pode retornar nulo, verificamos isso. O TypeScript sabe que se a unidade estiver correta, ela deve ser do tipo Unidade na ramificação if. Caso contrário, a unidade é inválida, o que significa que seu tipo é nulo (refinado de Unidade | nulo).
❹ Finalmente, retornamos null. Isso só pode acontecer se o usuário passar uma string para largura, mas essa string contiver unidades não suportadas.
Passei pela linha de pensamento do TypeScript para cada refinamento de tipo que foi feito. O TypeScript faz um ótimo trabalho pegando seu raciocínio enquanto você lê e escreve código e cristaliza-o em verificação de tipo e ordem de inferência.
Tipos de junção discriminados
Como acabamos de descobrir, o TypeScript tem um bom entendimento de como o JavaScript funciona e é capaz de rastrear nossas qualificações de tipo como se estivesse lendo nossas mentes.
Digamos que estejamos criando um sistema de eventos customizado para um aplicativo. Começamos definindo os tipos de eventos junto com as funções que tratam da chegada desses eventos. Imagine que UserTextEvent simula um evento de teclado (por exemplo, o usuário digitou <input />) e UserMouseEvent simula um evento de mouse (o usuário moveu o mouse para as coordenadas [100, 200]):
type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
// ...
return
}
event.value // [number, number]
}
O TypeScript sabe que dentro do bloco if, event.value deve ser uma string (graças ao typeof check), ou seja, event.value após o bloco if deve ser uma tupla [número, número] (por causa do retorno no bloco if).
Para onde vai levar a complicação? Vamos adicionar esclarecimentos aos tipos de eventos:
type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (typeof event.value === 'string') {
event.value // string
event.target // HTMLInputElement | HTMLElement (!!!)
// ...
return
}
event.value // [number, number]
event.target // HTMLInputElement | HTMLElement (!!!)
}
Enquanto o refinamento funcionou para event.value, não funcionou para event.target. Por quê? Quando o handle recebe um parâmetro do tipo UserEvent, não significa que você precisa passar UserTextEvent ou UserMouseEvent a ele - na verdade, você pode passar um argumento do tipo UserMouseEvent | UserTextEvent. E uma vez que os membros de uma união podem se sobrepor, o TypeScript precisa de uma maneira mais confiável de saber quando e qual caso de união é relevante.
Você pode fazer isso usando tipos literais e uma definição de tag para cada caso de tipo de união. Bela tag:
- Em cada caso, ele está localizado no mesmo local do tipo de sindicato. Implica o mesmo campo de objeto quando se trata de combinar tipos de objetos ou o mesmo índice quando se trata de combinar tuplas. Na prática, sindicatos discriminados são objetos com mais frequência.
- Digitado como um tipo literal (string literal, numérico, booleano etc.). Você pode misturar e combinar diferentes tipos de literais, mas é melhor manter um único tipo. Normalmente, este é um tipo de literal de string.
- Não é universal. As tags não devem receber argumentos de tipo genérico.
- Mutuamente exclusivo (único no tipo de união).
Vamos atualizar os tipos de eventos levando em consideração o acima:
type UserTextEvent = {type: 'TextEvent', value: string,
target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
if (event.type === 'TextEvent') {
event.value // string
event.target // HTMLInputElement
// ...
return
}
event.value // [number, number]
event.target // HTMLElement
}
Agora, quando refinamos o evento com base no valor de seu campo marcado (event.type), o TypeScript sabe que deve haver um UserTextEvent na ramificação if e, após a ramificação if, deve ser UserMouseEvent. Como as tags são únicas em cada tipo de união, o TypeScript sabe que eles são mutuamente exclusivos.
Use junções discriminadas ao escrever uma função que lida com vários casos de tipo de junção. Por exemplo, ao trabalhar com ações Flux, redux restaurações ou useReducer no React.
Você pode se familiarizar com o livro em mais detalhes e fazer o pré-pedido por um preço especial no site da editora