Búsqueda de sitios web

Problema con la relación de tipos llamables en TypeScript


Tengo el siguiente problema con la relación entre los tipos invocables en TypeScript:

type CheckExtends<T, U> = T extends U ? T : never

type NumberFunc = (op: number) => number
type AnyFunc = (op: any) => any;
type UnknownFunc = (op: unknown) => any;

type NumberAny = CheckExtends<NumberFunc, AnyFunc> // NumberFunc
type NumberUnknown = CheckExtends<NumberFunc, UnknownFunc> // never

(En el patio de juegos)

Se muestra a través de los comentarios que el tipo NumberFunc está extendiendo el tipo AnyFunc y no el tipo UnknownFunc.

No entiendo eso porque el tipo de parámetro de NumberFunc (number) se puede asignar tanto a los tipos any como a unknown

Respuesta #1

Tan pronto como introduces any, pierdes muchas de las garantías de seguridad de tipos en TypeScript. El tipo any es intencionalmente inseguro para permitirte hacer cosas que el compilador considera inseguras. Todo es tanto asignable a como asignable desde any; es una vía de escape del sistema de tipos. Por lo tanto, no es sorprendente que NumberFunc sea visto como asignable a AnyFunc. También habrías visto que AnyFunc es asignable a NumberFunc:

type AnyNumber = CheckExtends<AnyFunc, NumberFunc> // AnyFunc

Es mejor ignorar any, y pasar a la otra parte de tu pregunta usando el unknown seguro para tipos en su lugar: ¿por qué no se puede asignar NumberFunc a UnknownFunc?

Para preservar la seguridad de tipos, las funciones deberían ser contravariantes en sus tipos de parámetros. Esto significa que la relación entre subtipos/supertipos entre los tipos de funciones es opuesta a la de sus parámetros. Por ejemplo, aunque number es un subtipo de unknown, una función del tipo (x: number) => void es un supertipo de (x: unknown) => void. Así, la relación de asignabilidad se invierte. Esto puede ser confuso al principio, pero en realidad es lo único que tiene sentido desde la perspectiva de la seguridad de tipos. Esto es una consecuencia de lo que se conoce como el Principio de Sustitución de Liskov; si A es asignable a B, significa que puedes reemplazar cualquier valor del tipo B con uno del tipo A y no causará errores ni problemas.

Digamos que tengo un valor n de tipo number. Si pides un valor de tipo unknown, estarás contento si te doy n. Habrías estado contento si te hubiera dado "hello", o false, o cualquier valor. Porque number es un subtipo de unknown, puedes usar un number en cualquier lugar donde necesites un unknown.

Pero ahora imagina que tengo un valor f de tipo (x: number) => void. Si pides un valor de tipo (x: unknown) => void, y te doy f, podrías estar muy descontento cuando intentes usarlo. Piensas que tienes una función que acepta cualquier parámetro posible, por lo que esperas poder llamarla como f("hola"), o f(false), o f(15). Pero f() solo acepta parámetros de tipo number, así que f("hola") y f(false) es muy probable que provoquen errores en tiempo de ejecución. La función que estás pidiendo es muy específica. Como no puedes usar un (x: number) => void en algunos lugares donde sí puedes usar un (x: unknown) => void, entonces el primero no es un subtipo del segundo.

De hecho, es al revés: si tengo un valor g de tipo (x: unknown) => void, y tú pides un valor de tipo (x: number) => void, puedo entregarte g y estarás contento. Solo estarás llamando a g() con parámetros numéricos como g(1), g(2) o g(15), por lo que nunca tendrás un problema en tiempo de ejecución. Eso significa que UnknownFunc es un subtipo de NumberFunc:

type UnknownNumber = CheckExtends<UnknownFunc, NumberFunc> // UnknownFunc

Está bien, espero que eso ayude; ¡buena suerte!

Enlace de Playground al código

Respuesta #2

Como puedes ver en este artículo, las funciones de TypeScript solo se pueden asignar si los parámetros de la función destino son asignables a los parámetros de la función origen cuando strictFunctionChecks está habilitado. Por ejemplo, si tienes el siguiente código:

interface A {
    a: number;
}

interface B extends A {
    b: number;
}
type AFunc = (op: A) => any;
type BFunc = (op: B) => any;
let bf: BFunc = x => x;
let af: AFunc = bf;

Esto dará error porque no se puede asignar BFunc a Afunc debido a que A, el tipo de parámetro destino, no es asignable a B, el tipo de parámetro fuente. Tenga en cuenta que verificar si AFunc extends BFunc es equivalente a verificar si AFunc es asignable a BFunc. No se puede asignar NumberFunc a UnknownFunc porque unknown, el tipo de parámetro destino, no es asignable a number, el tipo de parámetro fuente. Esto significa que NumberFunc no extiende UnknownFunc. Si desactiva strictFunctionChecks en el playground, puede ver que NumberUnknown ya no es never y se convierte en NumberFunc.