Búsqueda de sitios web

typescript - ¿Cómo puedo hacer que la restricción de tipos funcione aquí?`


No estoy obteniendo el resultado que esperaba al restringir un tipo unión. Aquí hay un fragmento que captura el problema:

interface A {
    a : number;
}

interface B {
    b : string;
}

const isAnA = (arg : A | B) : arg is A => {
    return "a" in arg;
}

const applyFunc = <T>(func : (x : T) => number, arg : T) => {
    return func(arg)
}

const doTheThing = (arg : A | B) => {
    let f;
    if (isAnA(arg)) {
        f = (x : A) => x.a * 2;
    } else {
        f = (x : B) => parseInt(x.b) * 2;
    }
    return applyFunc(f, arg);
}

Lo que esperaba que sucediera aquí era que la función de guardia de tipo isAnA() hiciera que el compilador supiera que f tiene tipo (x : A) => number si arg es un A, y tipo (x : B) => number si arg es un B, lo que permitiría que applyFunc se llamara sobre arg y f juntos.

Sin embargo, obtengo esto del compilador:

  Type '(x: B) => number' is not assignable to type '(x: A) => number'.
    Types of parameters 'x' and 'x' are incompatible.
      Property 'b' is missing in type 'A' but required in type 'B'.

¿Hay alguna manera de hacer que esto funcione aparte de proteger explícitamente la llamada a applyFunc con un typeguard?

Respuesta #1

En términos generales, si deseas decirle a Typescript que dos variables tienen tipos correlacionados, es decir, "ya sea que arg sea A y func acepte A, o que arg sea B y func acepte B", entonces deben ser propiedades de algún objeto con un tipo de unión discriminada:

{arg: A, func: (x: A) => number} | {arg: B, func: (x: B) => number}

Para que realmente funcione en tu código, la unión discriminada necesita construirse usando un tipo condicional distributivo, para que el compilador no se queje de la función applyFuncWithObject. Resolví esto haciendo que ArgAndFunc sea un tipo parametrizado, así que la unión discriminada de arriba es ArgAndFunc<A | B>.

type ArgAndFunc<T> = T extends unknown ? {arg: T, func: (x: T) => number} : never

function applyFuncWithObject<T>(obj: ArgAndFunc<T>) {
    // or directly: return obj.func(obj.arg)
    return applyFunc(obj.func, obj.arg);
}

function doTheThing(arg: A | B) {
    let obj: ArgAndFunc<A | B>;
    if(isAnA(arg)) {
        obj = {arg, func: (x: A) => x.a * 2};
    } else {
        obj = {arg, func: (x: B) => parseInt(x.b) * 2};
    }
    return applyFuncWithObject<A | B>(obj);
}

Enlace del parque

No sé si esto cumplirá exactamente con tus necesidades, ya que no te permite pasar arg y func como dos argumentos separados en ningún lugar (sin escribir una función auxiliar genérica como applyFuncWithObject). Por otro lado, si realmente necesitas pasar arg y func juntos en varios lugares, entonces pasar un solo objeto probablemente simplificará tu código.

Respuesta #2

Una cosa que se podría hacer sería usar el operador condicional en su lugar: TypeScript funciona mejor cuando se minimiza la reasignación. Pero todavía existe el problema de que tienes el tipo (x: A => number) | (x: B) => number) que está indeseablemente separado del tipo del argumento (A | B).

Creo que el mejor enfoque aquí sería invocar la función necesaria dentro de la condición reducida, para evitar tener que volver a reducir más tarde, por ejemplo:

const doTheThing = (arg: A | B) => {
    return isAnA(arg)
        ? applyFunc((x: A) => x.a * 2, arg)
        : applyFunc((x: B) => parseInt(x.b) * 2, arg);
}