Búsqueda de sitios web

typescript - Comprobar si un parámetro genérico opcional es undefined resulta en T & ({} | null)


Así que estaba respondiendo a esta pregunta justo ahora, y en ella encontré algo extraño para lo cual no pude encontrar explicaciones.

Si tenemos una función genérica, y el parámetro es opcional y el tipo del parámetro está usando el genérico, algo así:

function ok<T>(thing?: T) {

}

Entonces, cuando intentamos restringir thing dentro de la función a solo T verificando explícitamente si es undefined:

thing !== undefined ? thing : 0;
//                    ^? T & ({} | null)

Entonces thing se restringe a T & ({} | null). Además, cuando usamos typeof, esto se restringe al esperado T. Esto también se comporta de la misma manera con las declaraciones if.

typeof thing !== "undefined" ? thing : 0;
//                             ^? T

Aquí tienes un ejemplo mínimo reproducible con todas las cosas que mencioné:

function ok<T>(thing?: T) {
    thing !== undefined ? thing : 0;
    //                    ^?

    if (thing !== undefined) {
        thing
    //  ^?
    }

    typeof thing !== "undefined" ? thing : 0;
    //                             ^?

    if (typeof thing !== "undefined") {
        thing
    //  ^?
    }
}

¿Por qué es este el caso? ¿Tienen las dos condiciones realmente un comportamiento diferente que yo no conocía? ¿Es esto un error? Estoy buscando una explicación para este comportamiento.

Respuesta #1

Este es el resultado de la mejora en la reducción de intersecciones, la compatibilidad de uniones y la restricción, tal como se implementa en microsoft/TypeScript#49119 y se lanzó en TypeScript 4.8.

Ciertos tipos de protecciones ahora reducirán tipos que antes no podían reducirse (¿es esa una palabra?) al intersectarlos con una versión filtrada de {} | null | undefined, un tipo unión equivalente al tipo unknown porque el tipo de objeto vacío {} acepta todos los valores excepto null y undefined (ver Cómo entender las relaciones entre los tipos any, unknown, {} y entre ellos y otros tipos para más información).

Específicamente, el PR menciona:

  • En el análisis de flujo de control de comparaciones de igualdad con null o undefined, los tipos genéricos se intersectan con {}, {} | null o {} | undefined en la rama falsa.

En tu caso, el valor thing comenzó siendo del tipo T | undefined, y una vez que eliminas undefined de eso con una verificación de igualdad, obtienes T & ({} | null) que representa de manera más precisa el hecho de que thing no puede ser undefined en comparación con T solo. Aunque la inferencia usualmente evitaría esto en tu ejemplo exacto, nada impide que alguien escriba ok<string | undefined>(Math.random()<0.5 ? "abc" : undefined), y entonces el tipo T sería string | undefined mientras que T & ({} | null) es solo string (no string & {}; consulta el PR para detalles sobre por qué y cómo esto es diferente).

Por otro lado, esta lógica no se implementó para la verificación conceptualmente equivalente typeof thing !== "undefined". La única verificación typeof que veo que se ve afectada aquí es para typeof xxx === "object". No está documentado allí si considerarían la diferencia en la restricción entre x === undefined y typeof x === "undefined" como un error, limitación de diseño, intencional o una función faltante, pero TypeScript definitivamente se está comportando según lo diseñado. Tal vez valdría la pena abrir un nuevo problema o al menos comentarlo en GitHub. No estoy seguro.

Respuesta #2

Preguntas muy interesantes.

Creo que la razón es algo así:

  • según esto, al usar un genérico no restringido T podría coincidir con ambos

    • null y
    • {}
  • esto te llevará a tener un tipo similar a T | null | {}.
  • De esto tiene sentido que al hacer T !== undefined se termine con algo similar a T & ({} | null).
  • pero al hacer un typeof para obtener resultados diferentes

También. Puedes verificar 2 cosas diferentes

  • Agrega una restricción al genérico. Algo como esto function ok<T extends string>. Esto llevará a resultados consistentes.
  • O usa != en lugar de !==, lo que convertirá thing en NonNullable<T>. Como se puede ver aquí

Parece que hace mucho, mucho tiempo {} era el tipo principal para genéricos y cambiarlo ahora rompería mucho código existente.

Artículos relacionados