TypeScript: Tipos avanzados, subtipos y varianza

TypeScript: Tipos avanzados, subtipos y varianza

Tabla de Contenido

Note

Octavo artículo de la serie sobre TypeScript. Abordaremos la teoría que sustenta el sistema de tipos: subtipos, supertipos, la jerarquía completa, los tipos unknown y never, y la varianza.

Comprender la relación entre tipos es lo que separa a quien usa TypeScript de quien lo domina. El compilador decide qué asignaciones son válidas aplicando reglas de subtipado y varianza. Una vez interiorizadas estas reglas, los errores del typechecker dejan de ser obstáculos crípticos y se convierten en información precisa sobre el diseño de tus tipos.

Subtipos

La regla fundamental del subtipado se enuncia así:

Info

Si tienes un tipo A y un tipo B donde B es subtipo de A, entonces puedes usar B en cualquier lugar donde se espere un A.

Algunas relaciones de subtipo en TypeScript:

  • Array es subtipo de Object.
  • Tuple es subtipo de Array.
  • Todo es subtipo de any.
  • never es subtipo de todo.
  • Si una clase Bird extiende Animal, entonces Bird es subtipo de Animal.

Aplicado a la práctica, esto significa que donde se necesite un Object puedes usar un Array; donde se necesite un Array puedes usar una Tuple; y donde se necesite un Animal puedes usar un Bird.

Supertipos

Un supertipo es la relación inversa:

Info

Si tienes un tipo A y un tipo B donde B es supertipo de A, entonces puedes usar A en cualquier lugar donde se espere un B.

Las relaciones de supertipo reflejan en espejo a las anteriores:

  • Array es supertipo de Tuple.
  • Object es supertipo de Array.
  • any es supertipo de todo.
  • Animal es supertipo de Bird.

La jerarquía de tipos

Todas estas relaciones forman una jerarquía con un tipo en la cúspide (top type) y otro en el fondo (bottom type):

flowchart TD
    Unknown[unknown - top type] --> Any[any]
    Any --> Number
    Any --> BigInt
    Any --> Boolean
    Any --> String
    Any --> Symbol
    Any --> Object
    Any --> Void
    Any --> Null
    Any --> Undefined
    Object --> Array
    Array --> Tuple
    Object --> Function
    Number --> Never[never - bottom type]
    String --> Never
    Boolean --> Never
    Object --> Never
    Tuple --> Never
  • unknown es el top type: todo es asignable a él, pero no permite operar sin acotar primero.
  • never es el bottom type: es subtipo de cualquier otro tipo y representa lo que nunca ocurre.

Los tipos especiales: null, undefined, void y never

Estos cuatro tipos modelan la ausencia de valor con matices distintos:

TipoSignificado
nullAusencia explícita de valor
undefinedVariable que aún no recibió un valor
voidFunción que no tiene sentencia return
neverFunción que nunca retorna

La distinción entre void y never es sutil pero importante. Una función void termina sin devolver nada útil; una función never no termina en absoluto, ya sea porque lanza una excepción o porque entra en un bucle infinito:

// void: termina, no retorna valor
function log(msg: string): void {
  console.log(msg)
}

// never: nunca completa su ejecución
function fail(message: string): never {
  throw new Error(message)
}

function loopForever(): never {
  while (true) {}
}

Tip

never es especialmente útil en la verificación de exhaustividad: si un switch cubre todos los casos de una unión, la rama default puede asignar el valor a un never, y el compilador detectará en tiempo de compilación cualquier caso olvidado al ampliar la unión.

Varianza

Determinar si un tipo es sub o supertipo de otro no siempre es evidente, sobre todo al combinar estructuras y funciones. TypeScript dispone de una notación formal para razonar sobre estas relaciones:

  • A <: B significa “A es subtipo de, o el mismo tipo que, B”.
  • A >: B significa “A es supertipo de, o el mismo tipo que, B”.

La varianza describe cómo se propagan estas relaciones cuando los tipos se anidan en estructuras (arreglos, objetos) o en firmas de función. Es el motivo por el que, por ejemplo, un arreglo de un subtipo no siempre es intercambiable con un arreglo de su supertipo: la mutabilidad introduce restricciones que el compilador debe respetar para mantener la solidez del sistema.

Conclusión

El sistema de tipos de TypeScript es una jerarquía gobernada por reglas de subtipado y varianza, con unknown como tipo superior y never como tipo inferior. Distinguir null, undefined, void y never, y comprender cómo B <: A permite sustituir un tipo por otro, te da una intuición precisa de por qué el compilador acepta o rechaza una asignación.

En el siguiente artículo aplicaremos metaprogramación con decoradores, funciones que extienden el comportamiento de clases, métodos y propiedades.

Etiquetas :