genéricos - ¿Cómo hago que los tipos de búsqueda de TypeScript funcionen en esta situación?
Perdón por el título poco motivador, pero realmente no tengo idea de cómo llamarlo de otra manera.
Tengo un enum, y quiero hacer coincidir cada una de las entradas del enum con un tipo. Estoy haciendo esto para que los parámetros de las funciones puedan cambiar dinámicamente según el primer parámetro. Un ejemplo:
enum Enum {
A,
B,
C
}
interface TypeMap {
[Enum.A]: number,
[Enum.B]: string,
[Enum.C]: boolean
}
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
//
}
El sistema de tipos funciona correctamente al llamar a doSomething, por ejemplo: doSomething(Enum.A, 5) funciona, pero doSomething(Enum.A, "hello") no. Sin embargo, lo que no puedo lograr que funcione es esto:
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
if (a === Enum.A) {
let num: number = b;
}
}
Typescript da error al asignar a num, pero claramente, CLARAMENTE, debería funcionar. Si a es Enum.A, entonces b tiene que ser POR DEFINICIÓN un number, ¿verdad? ¿Qué estoy haciendo mal? ¿Cómo hago para que esto funcione?
Respuesta #1
El problema principal aquí es que el análisis del flujo de control solo funciona para restringir los tipos de valores de tipo unión. No resulta en la restricción de parámetros de tipo genérico que extienden tipos unión. Que hayas probado a, de tipo K extends Enum, no restringe K en sí mismo. Hay un problema abierto en GitHub sobre esto: microsoft/TypeScript#24085.
Un problema con restringir K al verificar a es que nada impide que K sea el tipo de unión completo Enum. Por ejemplo:
function getEnum(): Enum { return Enum.A };
Si llamo a getEnum(), definitivamente obtendré Enum.A en tiempo de ejecución, pero el compilador solo ve el tipo de retorno como Enum, el tipo de unión completo. Por lo tanto, si llamas a doSomething(), el compilador permite esto:
doSomething(getEnum(), "oops"); // no error!
Vaya. Así que, de hecho, el error en la implementación de doSomething() en realidad te está advirtiendo sobre un problema real (aunque poco común): K podría ser Enum, a podría ser Enum.A, y b podría ser algo que no sea number.
Si pudieras decirle al compilador que K está restringido a ser exactamente un miembro de la unión Enum, entonces sería más seguro hacer la restricción. Actualmente no hay manera de expresar ese tipo de restricción genérica, pero hay un problema abierto solicitándolo: microsoft/TypeScript#27808.
Por ahora, entonces tendrás que sortear esto renunciando a cierta seguridad de tipos garantizada por el compilador, como usando afirmaciones de tipo:
function doSomethingAssert<K extends Enum>(a: K, b: TypeMap[K]) {
if (a === Enum.A) {
let num = b as number; // assert here
} else if (a === Enum.B) {
let str = b as string; // assert here
}
}
Esta es probablemente la solución menos disruptiva para ti. Podrías usar otras soluciones alternativas como el guardia de tipos definido por el usuario en la otra respuesta, pero tiene la misma falta de seguridad de tipos (nada te impide escribir let num=b as string, y nada te impide escribir isA(a, "oops") en el guardia de tipos). Así que depende de ti qué tipo de falta de solidez prefieres.
Una última idea: ¿quizás considerarías reestructurar tus datos para usar una unión discriminada en lugar de un par de parámetros de función? ¿Y que ya no sea genérico? El compilador es mucho mejor usando el análisis de flujo de control en objetos de unión discriminada. Entonces empacarías a y b en un único tipo de objeto, así:
type DiscrimUnion = { [K in Enum]: { a: K, b: TypeMap[K] } }[Enum]
// type DiscrimUnion = { a: Enum.A; b: number;} | { a: Enum.B; b: string;} |
// { a: Enum.C; b: boolean;}
Y entonces la implementación funciona de la manera que deseas:
function doSomethingDiscrimUnion(u: DiscrimUnion) {
if (u.a === Enum.A) {
let num: number = u.b;
} else if (u.a === Enum.B) {
let str: string = u.b;
}
}
y tiene mejores garantías sobre cómo puedes llamarlo:
doSomethingDiscrimUnion({a: Enum.A, b: 123}); // okay
doSomethingDiscrimUnion({a: getEnum(), b: "oops"}); // error! not a DiscimUnion
Está bien, espero que eso ayude; ¡buena suerte!
Enlace de Playground al código
Respuesta #2
Puede que haya encontrado una solución usando predicados de tipo. Como alternativa a la forma clásica de hacer la comprobación de tipos (a === Enum.A) podemos definir una función en la que pasamos tanto A como B y restringimos el tipo de B con respecto al valor de A usando predicados de tipo (básicamente la palabra clave is). Así:
function isA(a: Enum, b: any): b is TypeMap[Enum.A] {
return a === Enum.A
}
//function isB
//function isC
function doSomething<K extends Enum>(a: K, b: TypeMap[K]) {
if (isA(a, b)) {
let x: number = b;
}
}
Al hacerlo, podemos permitir que el compilador reduzca con éxito el tipo de B.
Puedes consultar esto en este enlace.
