Búsqueda de sitios web

Covarianza con undefined en TypeScript


El siguiente código está claramente tipado incorrectamente, sin embargo, no puedo hacer que TypeScript genere un error sobre él. Activé strict, así como strictNullChecks y strictFunctionTypes por si acaso, pero TS sigue firme en su convicción de que este código está bien y es adecuado.

abstract class A {
    // You can pass an undefined to any A
    public abstract foo(_x: number | undefined);
}

class B extends A {
    // B is an A, but it prohibits passing in an undefined.
    // Note that if we did `x: string`, TS would flag it as
    // an error.
    public foo(x: number) {
        if (x === undefined) {
            throw new Error("Type error!");
        }
    }
}

function makeAnA(): A {
    // This typechecks correct, so B is clearly an A, in
    // TS's opinion.
    return new B();
}

function test() {
    const b = makeAnA();
    // b is a B, so this should not be possible
    b.foo(undefined);
}

¿Es este un comportamiento esperado? ¿Hay alguna opción que pueda activar que marque esto como un error? Esto me ha causado problemas más de una vez.

Respuesta #1

Esta es una decisión de diseño. Todos los parámetros de los métodos se comportan de manera bivariada. Esto significa que, en lo que a TypeScript respecta, para los métodos (_x: number) => void es un subtipo de (_x: number | number) => void (y viceversa). Esto es obviamente inseguro.

Inicialmente, no solo los parámetros de los métodos se comportaban de manera bivariada, sino todos los parámetros de las firmas de funciones. Para solucionar esto, se añadió la bandera strictFunctionTypes en TypeScript 2.6. Del PR:

Con este PR introducimos un modo --strictFunctionTypes en el cual las posiciones de los parámetros de tipo de función se verifican de manera contravariante en lugar de bivarianamente. La verificación más estricta se aplica a todos los tipos de función, excepto a aquellos que se originan en declaraciones de métodos o constructores. Los métodos se excluyen específicamente para asegurar que las clases e interfaces genéricas (como Array) continúen relacionándose principalmente de manera covariante. El impacto de verificar estrictamente los métodos sería un cambio mucho más disruptivo, ya que un gran número de tipos genéricos se volverían invariantes (aun así, podríamos continuar explorando este modo más estricto).

(resaltado agregado)

Así que aquí obtenemos un vistazo a la decisión de que los parámetros de los métodos continúen relacionándose de manera bivariada. Es por conveniencia. Sin esta falta de solidez, la mayoría de las clases serían invariantes. Por ejemplo, si Array fuera invariante, Array<Dog> no sería un subtipo de Array<Animal>, creando todo tipo de puntos problemáticos en código bastante básico.

Aunque definitivamente no es equivalente, si usamos un campo de función en lugar de un método (con strictFunctionTypes activado), obtenemos un error que dice Type '(x: number) => void' is not assignable to type '(_x: number | undefined) => void'

abstract class A {
    // You can pass an undefined to any A
    public foo!: (_x: number | undefined) => void;
}

class B extends A {
    // Error here
    public foo: (x: number) => void = x => {
        if (x === undefined) {
            throw new Error("Type error!");
        }
    }
}

function makeAnA(): A {
    //And here 
    return new B();
}

function test() {
    const b = makeAnA();
    // b is a B, so this should not be possible
    b.foo(undefined);
}

Enlace del parque

Nota: El código anterior da un error solo con strictFunctionTypes, ya que sin él todos los parámetros de función continúan comportándose de manera bivariada.

Artículos relacionados