Mecanografiado. Tipos avanzados

imagen



¡Hola habitantes! Hemos presentado otra novedad

" Professional TypeScript. Desarrollo de aplicaciones JavaScript escalables " a la imprenta . En este libro, los programadores que ya son intermedios en JavaScript aprenderán a dominar TypeScript. Verá cómo TypeScript puede ayudarlo a escalar su código hasta 10 veces mejor y hacer que la programación sea divertida nuevamente.



A continuación se muestra un extracto de un capítulo del libro "Tipos avanzados".



Tipos avanzados



El sistema de tipos TypeScript, reconocido mundialmente, sorprende incluso a los programadores de Haskell con sus capacidades. Como ya sabe, no solo es expresivo, sino también fácil de usar: las restricciones de tipo y las relaciones en él son concisas, comprensibles y, en la mayoría de los casos, se deducen automáticamente.



Modelar elementos de JavaScript dinámicos como prototipos, enlazar esto, sobrecargas de funciones y objetos en constante cambio requiere un sistema de tipos y un sistema de tipos tan ricos como los que Batman aceptaría.



Comenzaré este capítulo con una inmersión profunda en los temas de subtipificación, compatibilidad, varianza, variable aleatoria y extensión. Luego, ampliaré los detalles de la verificación de tipos basada en flujo, incluido el refinamiento y la totalidad. A continuación, demostraré algunas características de programación avanzadas a nivel de tipo: conectar y mapear tipos de objetos, usar tipos condicionales, definir protecciones de tipos y soluciones alternativas como aserciones de tipo y aserciones de asignación explícitas. Finalmente, le presentaré algunos patrones avanzados para mejorar la seguridad de los tipos: patrones de objetos complementarios, mejoras de la interfaz para tuplas, imitación de tipos nominales y extensión de prototipos seguros.



Relaciones entre tipos



Echemos un vistazo más de cerca a las relaciones en TypeScript.



Subtipos y supertipos



Ya hemos mencionado la compatibilidad en la sección Acerca de los tipos en la p. 34, así que profundicemos en este tema, comenzando con la definición del subtipo.

imagen




Regrese a la fig. 3.1 y vea las asociaciones de subtipos integradas de TypeScript.

imagen




  • Array es un subtipo de objeto.
  • Una tupla es un subtipo de una matriz.
  • Todo es un subtipo de cualquiera.
  • nunca es un subtipo de todo.
  • La clase Bird, que amplía la clase Animal, es un subtipo de la clase Animal.


Según la definición que acabo de dar para un subtipo, esto significa que:



  • Donde sea que necesite un objeto, puede usar una matriz.
  • Siempre que se necesite una matriz, se puede utilizar una tupla.
  • Donde sea que lo necesite, puede usar un objeto.
  • Nunca se puede usar en todas partes.
  • Donde sea que necesite un animal, puede usar Bird.


Un supertipo es lo opuesto a un subtipo.



SUPERTIPO



Si tiene dos tipos, A y B, y B es un supertipo de A, entonces puede usar A con seguridad donde sea que se necesite B (Figura 6.2).


imagen


Y de nuevo, según el diagrama de la Fig. 3.1:



  • Array es un supertipo de tupla.
  • El objeto es un supertipo de matriz.
  • Cualquiera es un supertipo de todo.
  • Nunca no es el supertipo de nadie.
  • Animal es un supertipo de pájaro.


Es todo lo contrario de los subtipos y nada más.



Variación



Para la mayoría de los tipos, es bastante fácil entender si un cierto tipo A es un subtipo de B. Para tipos simples como número, cadena, etc., puede consultar el diagrama de la Fig. 3.1 o determinar de forma independiente que el número contenido en el número de unión | cadena es un subtipo de esta unión.



Pero hay tipos más complejos, como los genéricos. Considere estas preguntas:



  • ¿Cuándo es Array <A> un subtipo de Array <B>?
  • ¿Cuándo es la Forma A un subtipo de la Forma B?
  • ¿Cuándo es la función (a: A) => B un subtipo de función (c: C) => D?


Las reglas de subtipificación para tipos que contienen otros tipos (es decir, tener parámetros de tipo como Array <A>, formularios con campos como {a: número} o funciones como (a: A) => B) son más difíciles de comprender, porque no son coherente en diferentes lenguajes de programación.



Para facilitar la lectura de las siguientes reglas, presentaré algunos elementos de sintaxis que no funcionan en TypeScript (no se preocupe, no es matemático):



  • A <: B significa "A es un subtipo del mismo que el tipo B";
  • A>: B significa "A es un supertipo del mismo tipo que el tipo B".


Variación de forma y matriz



Para comprender por qué los idiomas no concuerdan en las reglas para subtipificar tipos complejos, será útil un ejemplo con una forma que describa a un usuario en una aplicación. Lo representamos a través de un par de tipos:



//  ,   .
type ExistingUser = {
    id: number
   name: string
}
//  ,     .
type NewUser = {
   name: string
}


Suponga que un pasante de su empresa tiene la tarea de escribir código para eliminar un usuario. Comienza con lo siguiente:



function deleteUser(user: {id?: number, name: string}) {
    delete user.id
}
let existingUser: ExistingUser = {
    id: 123456,
    name: 'Ima User'
}
deleteUser(existingUser)


deleteUser recibe un objeto de tipo {id?: número, nombre: cadena} y le pasa un usuario existente de tipo {id: número, nombre: cadena}. Tenga en cuenta que el tipo de identificación de propiedad (número) es un subtipo del tipo esperado (número | indefinido). Por lo tanto, todo el objeto {id: number, name: string} es un subtipo de {id?: Number, name: string}, por lo que TypeScript lo permite.



¿Ves algún problema de seguridad? Hay uno: después de pasar ExistingUser a deleteUser, TypeScript no sabe que se eliminó el ID de usuario, por lo que si lee existingUser.id después de eliminarlo deleteUser (existingUser), TypeScript aún asumirá que existingUser.id es del tipo número.



Obviamente, usar un tipo de objeto donde se espera su supertipo no es seguro. Entonces, ¿por qué TypeScript permite esto? La conclusión es que no estaba destinado a ser completamente seguro. Su sistema de tipos busca captar errores reales y hacerlos visibles para programadores de todos los niveles. Dado que las actualizaciones destructivas (como eliminar una propiedad) son relativamente raras en la práctica, TypeScript es relajado y le permite asignar un objeto donde se espera su supertipo.



Y qué pasa con el caso contrario: ¿es posible asignar un objeto donde se espera su subtipo?



Agreguemos un nuevo tipo para el usuario anterior y luego eliminemos el usuario con ese tipo (imagine agregar tipos al código que escribió su colega):



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


Cuando envía un formulario con una propiedad cuyo tipo es un supertipo del tipo esperado, TypeScript lo jura. Esto se debe a que id es una cadena | numero | undefined y deleteUser solo maneja el caso donde id es número | indefinido.



Mientras espera un formulario, puede pasar un tipo con tipos de propiedad que sean <: de los tipos esperados, pero no puede pasar un formulario sin tipos de propiedad que sean supertipos de sus tipos esperados. Cuando hablamos de tipos, decimos, "Las formas de TypeScript (objetos y clases) son covariantes en los tipos de sus propiedades". Es decir, para que el objeto A se asigne al objeto B, cada una de sus propiedades debe ser <: la propiedad correspondiente en B. La



covarianza es uno de los cuatro tipos de varianza:



Invarianza

Específicamente necesaria T.

Covarianza

Necesario <: T.

Contravarianza

necesaria>: T.

La bivariancia se adaptará

a <: T o>: T.



En TypeScript, cada tipo complejo es covariante en sus miembros (objetos, clases, matrices y tipos de retorno de función) con una excepción: los tipos de parámetros de función son contravariantes.



. , . ( ). , Scala, Kotlin Flow, , .



TypeScript : , , , (, id deleteUser, , , ).


Variación de la función



Consideremos algunos ejemplos.



La función A es un subtipo de función B si A tiene la misma o menor aridad (número de parámetros) que B, y:



  1. El tipo this, que pertenece a A, es indefinido, o>: del tipo this, que pertenece a B.
  2. Cada uno de los parámetros A>: el parámetro correspondiente en B.
  3. Tipo de retorno A <: tipo de retorno B.


Tenga en cuenta que para que la función A sea un subtipo de la función B, su este tipo y los parámetros deben ser>: contrapartes en B, mientras que su tipo de retorno debe ser <:. ¿Por qué se revierte la condición? ¿Por qué la condición <: simple no funciona para cada componente (de tipo this, tipos de parámetros y tipo de retorno), como es el caso de objetos, matrices, uniones, etc.?



Comencemos por definir tres tipos (en lugar de clase, puede usar otros tipos, donde A: <B <: C):



class Animal {}
class Bird extends Animal {
    chirp() {}
}
class Crow extends Bird {
    caw() {}
}


Entonces Crow <: Bird <: Animal.



Definamos una función que tome Bird y lo haga tuitear:



function chirp(bird: Bird): Bird {
    bird.chirp()
    return bird
}


Hasta ahora tan bueno. ¿Qué te permite TypeScript hacer chirriar?



chirp(new Animal) //  TS2345:   'Animal'
chirp(new Bird) //     'Bird'.
chirp(new Crow)


Una instancia de Bird (como un parámetro de chirrido de tipo pájaro) o una instancia de Crow (como un subtipo de Bird). El paso de subtipos funciona como se esperaba.



Creemos una nueva función. Esta vez su parámetro será una función:



function clone(f: (b: Bird) => Bird): void {
    // ...
}


clon requiere una función f que recibe Bird y devuelve Bird. ¿Qué tipos de funciones se pueden pasar af de forma segura? Obviamente, la función que recibe y devuelve Bird:



function birdToBird(b: Bird): Bird {
    // ...
}
clone(birdToBird) // OK


¿Qué pasa con una función que toma un pájaro pero devuelve un cuervo o un 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 como se esperaba, pero birdToAnimal arroja un error. ¿Por qué? Imagina que una implementación de clonación se ve así:



function clone(f: (b: Bird) => Bird): void {
    let parent = new Bird
    let babyBird = f(parent)
    babyBird.chirp()
}


Al pasar la función f para clonar, que devuelve Animal, no podemos llamar a .chirp en ella. Por lo tanto, TypeScript debe asegurarse de que la función que pasamos devuelva al menos Bird.



Cuando decimos que las funciones son covariantes en sus tipos de retorno, significa que una función puede ser un subtipo de otra función solo si su tipo de retorno es <: el tipo de retorno de esa función.



Bien, ¿qué pasa con los tipos de parámetros?



function animalToBird(a: Animal): Bird {
  // ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
  // ...
}
clone(crowToBird)        //  TS2345:   '(c: Crow) =>
                        // Bird'     '
                       // (b: Bird) => Bird'.


Para que una función sea compatible con otra función, todos sus tipos de parámetros (incluido este) deben ser>: sus parámetros correspondientes en la otra función. Para entender por qué, piense en cómo el usuario podría implementar crowToBird antes de pasarlo a clonar.



function crowToBird(c: Crow): Bird {
  c.caw()
  return new Bird
}


TSC-: STRICTFUNCTIONTYPES



- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.



{«strict»: true}, .


Ahora, si clon llama a crowToBird con new Bird, obtenemos una excepción, porque .caw está definido en todos los Crow, pero no en todos los Birds.



Esto significa que las funciones son contravariantes en sus parámetros y este tipo. Es decir, una función puede ser un subtipo de otra función solo si cada uno de sus parámetros y el tipo this son>: sus parámetros correspondientes en la otra función.



Afortunadamente, no es necesario memorizar estas reglas. Solo recuérdelos cuando el editor dé un subrayado rojo cuando pase una función escrita incorrectamente en algún lugar.



Compatibilidad



Las relaciones de subtipo y supertipo son un concepto clave en cualquier lenguaje escrito estáticamente. También son importantes para comprender cómo funciona la compatibilidad (recuerde, la compatibilidad se refiere a las reglas de TypeScript que rigen el uso del tipo A cuando se requiere el tipo B).



Cuando TypeScript necesita responder a la pregunta "¿El tipo A es compatible con el tipo B?", Sigue reglas simples. Para los tipos que no son enum, como matrices, valores booleanos, números, objetos, funciones, clases, instancias de clases y cadenas, incluidos los tipos literales, A es compatible con B si una de las condiciones es verdadera.



  1. A <: B.
  2. A es cualquiera.


La regla 1 es solo una definición de subtipo: si A es un subtipo de B, entonces, siempre que se necesite B, puede usar A. La



regla 2 es una excepción a la regla 1 para facilitar la interacción con el código JavaScript.

Para los tipos de enumeración creados por las palabras clave enum o const enum, el tipo A es compatible con la enumeración B si una de las condiciones es verdadera.



  1. A es miembro de la enumeración B.
  2. B tiene al menos un miembro de tipo número y A es número.


La regla 1 es exactamente la misma que para los tipos simples (si A es miembro de B, entonces A es de tipo B y decimos B <: B).



La regla 2 es necesaria para la conveniencia de trabajar con enumeraciones, que comprometen seriamente la seguridad de TypeScript (consulte la subsección “Enum” en la página 60), y recomiendo evitarlas.



Expansión de tipos La expansión de tipos



es la clave para comprender cómo funciona la inferencia de tipos. TypeScript es indulgente en la ejecución y es más probable que se equivoque al deducir un tipo más general que al deducir lo más específico posible. Esto le facilitará la vida y reducirá el tiempo que lleva lidiar con las notas de tipo corrector.



Ya ha visto varios ejemplos de expansión de tipos en el Capítulo 3. Considere a otros.



Cuando declaras una variable como mutable (con let o var), su tipo se expande desde el tipo de valor de su literal al tipo base al que pertenece el literal:



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


Esto no se aplica a las declaraciones inmutables:



const a = 'x' // 'x'
const b = 3   // 3
const c = true   // true
enum E {X, Y, Z}
const e = E.X   // E.X


Puede usar una anotación de tipo explícita para evitar que 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}


Cuando reasigna un tipo no extendido con let o var, TypeScript lo extiende por ti. Para evitar esto, agregue una anotación de tipo explícita a la declaración original:



const a = 'x' // 'x'
let b = a  // string
const c: 'x' = 'x'  // 'x'
let d = c  // 'x'


Las variables inicializadas a nulas o indefinidas se expanden a cualquiera:



let a = null // any
a = 3  // any
a = 'b'  // any


Pero, cuando una variable inicializada como nula o indefinida abandona el ámbito en el que fue declarada, TypeScript le asigna un tipo específico:



function x() {
   let a = null  // any
   a = 3   // any
   a = 'b'   // any
   return a
}
x()   // string


El



tipo const El tipo const ayuda a evitar extender la declaración de tipo. Úselo como una afirmación de tipo (consulte la subsección "Aprobaciones de tipo" en la 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 la expansión de tipos y marca recursivamente sus miembros como de solo lectura, incluso en estructuras de datos profundamente anidadas:



let d = [1, {x: 2}]              // (number | {x: number})[]
let e = [1, {x: 2}] as const    // readonly [1, {readonly x: 2}]


Utilice como constante cuando desee que TypeScript deduzca lo más estrecho posible.



Comprobación de propiedades adicionales



La expansión de tipos también entra en juego cuando TypeScript comprueba si un tipo de objeto es compatible con otro tipo de objeto.



Los tipos de objetos son covariantes en sus miembros (consulte la subsección “Variación de forma y matriz” en la página 148). Pero, si TypeScript sigue esta regla sin verificaciones adicionales, pueden surgir problemas.



Por ejemplo, considere un objeto Options que puede pasar a una clase para personalizarlo:



type Options = {
    baseURL: string
    cacheSize?: number
    tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({
     baseURL: 'https://api.mysite.com',
     tier: 'prod'
})


¿Qué pasa ahora si comete un error en la opción?



new API({
   baseURL: 'https://api.mysite.com',
   tierr: 'prod'         //  TS2345:   '{tierr: string}'
})                      //     'Options'.
                        //     
                       //  ,  'tierr'  
                      //   'Options'.    'tier'?


Este es un error común de JavaScript y es bueno que TypeScript lo ayude a detectarlo. Pero si los tipos de objetos son covariantes en sus miembros, ¿cómo los intercepta TypeScript?



En otras palabras:



  • Esperábamos el tipo {baseURL: string, cacheSize?: Number, tier?: 'Prod' | 'dev'}.
  • Pasamos el tipo {baseURL: string, tierr: string}.
  • El tipo pasado es un subtipo del tipo esperado, pero TypeScript supo informar un error.


Al comprobar las propiedades adicionales , cuando intenta asignar un nuevo tipo de literal de objeto T a otro tipo, U y T tiene propiedades que U no tiene, TypeScript informa un error.



El nuevo tipo de literal de objeto es el tipo que TypeScript infiere de un objeto literal. Si este literal de objeto utiliza una aserción de tipo (consulte la subsección “Aserciones de tipo” en la página 185) o se asigna a una variable, el nuevo tipo se expande al tipo de objeto normal y se pierde su novedad.



Intentemos hacer que esta definición sea más amplia:



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)


❶ Cree una instancia de la API con baseURL y una de las dos propiedades opcionales: tier. Todo esta funcionando



❷ Escribimos por error tier como badTier. El objeto de opciones que pasamos a la nueva API es nuevo (se infiere su tipo, es incompatible con la variable y no escribimos aserciones para él), por lo que al verificar propiedades innecesarias, TypeScript detecta una propiedad badTier adicional (que se define en el objeto de opciones, pero no en el tipo Opciones).



❸ Hacer una declaración de que el objeto de opciones no válidas es de tipo Opciones. TypeScript ya no lo ve como nuevo y, al comprobar las propiedades adicionales, concluye que no hay errores. La sintaxis as T se describe en la sección "Aserciones de tipo" en la p. 185.



❹ Asignar el objeto de opciones a la variable badOptions. TypeScript ya no lo percibe como nuevo y, después de verificar las propiedades innecesarias, concluye que no hay errores.



❺ Cuando escribimos explícitamente opciones como Opciones, el objeto que asignamos a las opciones es nuevo, por lo que TypeScript busca propiedades adicionales y encuentra un error. Tenga en cuenta que en este caso, la verificación de propiedades adicionales no se realiza cuando pasamos opciones a la nueva API, pero sí cuando intentamos asignar un objeto de opciones a la variable de opciones.



No es necesario que memorice estas reglas. Esta es solo una heurística interna de TypeScript para detectar tantos errores como sea posible. Solo téngalos en cuenta si de repente se pregunta cómo TypeScript se enteró de un error que incluso Ivan, el veterano de su empresa y también un censor de código profesional, no notó.



El refinamiento de



TypeScript realiza la ejecución simbólica de la inferencia de tipos. El módulo de verificación de tipos utiliza instrucciones de flujo de comandos (como if,?, || y switch) junto con consultas de tipos (como typeof, instanceof e in), calificando así los tipos cuando un programador lee el código. Sin embargo, esta práctica función se admite en muy pocos idiomas.



Imagine que ha desarrollado una API para definir reglas CSS en TypeScript y su colega quiere usarla para establecer el ancho de un elemento HTML. Pasa el ancho que desea analizar y verificar más tarde.



Primero, implementemos una función para analizar una cadena CSS en valor y unidad:



//       
//  ,      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
}


Luego usamos parseUnit para analizar el ancho proporcionado por el usuario. el ancho puede ser un número (posiblemente en píxeles) o una cadena con las unidades adjuntas, o nulo o indefinido.



En este ejemplo, usamos la calificación de tipo varias veces:



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
}


❶ TypeScript es capaz de comprender que la igualdad flexible de JavaScript contra null devolverá verdadero tanto para null como para undefined. También sabe que si el cheque pasa, entonces haremos una devolución, y si no hacemos una devolución, entonces el cheque falló y desde ese momento, el tipo de ancho es número | cadena (ya no puede ser nulo o indefinido). Decimos que el tipo se refinó a partir del número | cadena | nulo | indefinido en número | cuerda.



❷ El tipo de verificación solicita un valor en tiempo de ejecución para ver su tipo. TypeScript también aprovecha typeof en tiempo de compilación: en la rama if donde pasa la prueba, TypeScript sabe que el ancho es número. De lo contrario (si esta rama regresa) el ancho debería ser una cadena, el único tipo restante.



❸ Dado que parseUnit puede devolver nulo, verificamos esto. TypeScript sabe que si unit es correcta, entonces debe ser de tipo Unit en la rama if. De lo contrario, la unidad no es válida, lo que significa que su tipo es nulo (refinado de Unidad | nulo).



❹ Finalmente, devolvemos nulo. Esto solo puede suceder si el usuario pasa una cadena de ancho, pero esa cadena contiene unidades no admitidas.

Pasé por el tren de pensamiento de TypeScript para cada refinamiento de tipo que se hizo. TypeScript hace un gran trabajo al tomar su razonamiento mientras lee y escribe código y lo cristaliza en la verificación de tipos y el orden de inferencia.



Tipos de unión discriminados



Como acabamos de descubrir, TypeScript tiene una buena comprensión de cómo funciona JavaScript y es capaz de rastrear nuestras calificaciones de tipo como si leyera nuestras mentes.



Digamos que estamos creando un sistema de eventos personalizado para una aplicación. Comenzamos por definir los tipos de eventos junto con las funciones que manejan la llegada de estos eventos. Imagine que UserTextEvent simula un evento de teclado (por ejemplo, el usuario escribió el texto <input />) y UserMouseEvent simula un evento de mouse (el usuario movió el mouse en las 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]
}


TypeScript sabe que dentro del bloque if, event.value debe ser una cadena (gracias al tipo de verificación), es decir, event.value después del bloque if debe ser una tupla [number, number] (debido al retorno en el bloque if).



¿A qué conducirá la complicación? Agreguemos aclaraciones a los 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 (!!!)
}


Si bien el refinamiento funcionó para event.value, no lo hizo para event.target. ¿Por qué? Cuando handle recibe un parámetro de tipo UserEvent, no significa que deba pasar UserTextEvent o UserMouseEvent; de hecho, puede pasar un argumento de tipo UserMouseEvent | UserTextEvent. Y dado que los miembros de una unión pueden superponerse, TypeScript necesita una forma más confiable de saber cuándo y qué caso de unión es relevante.



Puede hacer esto usando tipos literales y una definición de etiqueta para cada caso de tipo de unión. Bonita etiqueta:



  • En cada caso, se ubica en el mismo lugar del tipo de unión. Implica el mismo campo de objeto cuando se trata de combinar tipos de objetos, o el mismo índice cuando se trata de combinar tuplas. En la práctica, las uniones discriminadas suelen ser objetos.
  • Escrito como un tipo literal (literal de cadena, numérico, booleano, etc.). Puede mezclar y combinar diferentes tipos de literales, pero es mejor ceñirse a un solo tipo. Normalmente, este es un tipo de literal de cadena.
  • No es universal. Las etiquetas no deben recibir argumentos de tipo genérico.
  • Mutuamente excluyentes (únicos dentro del tipo de unión).


Actualicemos los tipos de eventos teniendo en cuenta lo anterior:



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
}


Ahora, cuando refinamos el evento en función del valor de su campo etiquetado (event.type), TypeScript sabe que debería haber un UserTextEvent en la rama if, y después de la rama if, debería tener un UserMouseEvent. Debido a que las etiquetas son únicas en cada tipo de unión, TypeScript lo sabe que son mutuamente excluyentes.



Utilice combinaciones discriminadas al escribir una función que maneje varios casos de tipo de combinación. Por ejemplo, cuando trabaje con acciones de Flux, redux restaura o useReducer en React.



Puede familiarizarse con el libro con más detalle y realizar un pedido por adelantado a un precio especial en el sitio web del editor.



All Articles