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
nulloundefined, los tipos genéricos se intersectan con{},{} | nullo{} | undefineden 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
Tpodría coincidir con ambosnully
{}
T | null | {}.T !== undefined se termine con algo similar a T & ({} | null).typeof para obtener resultados diferentesTambié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áthingenNonNullable<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.