Búsqueda de sitios web

typescript - Quiero definir un tipo de función de callback, un parámetro es booleano, el tipo del otro parámetro depende de este tipo booleano


Estoy escribiendo un tipo de función de retorno.

Un parámetro llamado A es booleano, otro parámetro llamado B depende de este parámetro booleano:

  • si A es verdadero, B será un objeto

  • si A es falso, B será indefinido

así

login({callback : (success , res) => {
    if(success === true){
        console.log(res.a) //ok
    } else{
        console.log(res.a) // error: res is undefined
    }
}})

Escribí un tipo pero no funciona

interface Options {
    callback?: <T extends boolean>(success: T ,res: T extends true? {a : string}:never) => void;
}

login({callback : (success , res) => {
    if(success === true){
        console.log(res.a) //ok
    } else{
        console.log(res.a) // also ok
    }
}})

Respuesta #1

Desde el punto de vista de la implementación de login(), sería mejor que la propiedad callback del parámetro de la función aceptara argumentos que sean o bien de tipo [true, {a: string}] o de tipo [false, undefined?]. Tradicionalmente, escribirías esto como una función sobrecargada con múltiples firmas de llamada:

function login({ callback }: {
  callback: {
    (success: true, res: { a: string }): void;
    (success: false, res?: undefined): void;
  }
}) {
  callback(true, { a: "hey" });
  callback(false);
}

Obtendrás un poco más de seguridad de tipo si en su lugar representas la función como tomando un parámetro rest de un tipo de tupla, o mejor dicho, una unión de tipos de tupla:

function login({ callback }: {
  callback: (...args:
    [success: true, res: { a: string }] |
    [success: false, res?: undefined]
  ) => void
}) {
  callback(true, { a: "hey" });
  callback(false);
}

Al llamar a callback(true, {a: "hey"}) o callback(false), ambas versiones funcionan, pero realmente no se puede obtener seguridad de tipos al implementar el callback en sí con sobrecargas. Para el resto de esta respuesta, asumiré que estás usando el enfoque de unión de tupla con parámetros rest.


La unión [success: true, res: {a: string}] | [success: false, res?: undefined] es una unión discriminada, donde el primer elemento (con un índice de 0) es la propiedad discriminante. Si tienes un valor args, puedes verificar args[0] para determinar si el segundo elemento (con un índice de 1) es un {a: string} o undefined.

Entonces, al llamar a login(), puedes obtener seguridad de tipos si usas args como un parámetro rest y verificas sus índices 0 y 1:

login({
  callback: (...args) => {
    if (args[0]) {
      console.log(args[1].a) // okay
    } else {
      console.log(args[1].a) // error: undefined
    }
  }
})

Pero no puedes obtener la misma seguridad con una función flecha que toma dos parámetros separados en lugar de un parámetro rest:

login({
  callback: (success, res?) => {
    if (success) {
      console.log(res.a) // error!
    } else {
      console.log(res.a) // error
    }
  }
})

El compilador no realiza un seguimiento de la correlación entre success y res. Esta es una limitación actual en TypeScript. Todavía no realiza el seguimiento de la correlación si utilizas asignación por desestructuración:

login({
  callback: (...[success, res]) => {
    if (success) {
      console.log(res.a) // error!
    } else {
      console.log(res.a) // error
    }
  }
})

Ambos se enfrentan al mismo problema, donde asignar las partes de la unión a las variables success y res rompe la correlación. TypeScript 4.4 agregó una característica, como se implementa en microsoft/TypeScript#44730, que ayuda un poco, ya que permite usar success para acotar args, pero aún no permite usar success para acotar res:

login({
  callback: (...args) => {
    const [success, res] = args;
    if (success) {
      console.log(res.a) // error
      console.log(args[1].a) //ok
    }
  }
})

En esa solicitud de extracción dice:

El patrón de desestructurar una propiedad discriminante y una propiedad de carga útil en dos variables locales y esperar un acoplamiento entre las dos no es compatible, ya que el analizador de flujo de control no "ve" la conexión. ... Es posible que podamos admitir ese patrón más adelante, pero probablemente no en este PR.

Así que hay algo de esperanza de que en una versión futura de TypeScript puedas escribir (success, res?) => ... y las cosas funcionen mágicamente. Por ahora, sin embargo, tienes que refactorizar tu código un poco.


Y esto, por supuesto, deja de lado una refactorización más simple: simplemente olvida success por completo y usa res en su lugar:

function fixLogin(cb: (res?: { a: string } | undefined) => void) {
  login({ callback: (success, res?) => cb(res) });
}

fixLogin((res?) => {
  if (res) {
    console.log(res.a) // okay
  } 
})

Después de todo, success no te da ninguna información adicional sobre res. Si res está presente y definido, entonces success tenía que ser true, y si res es undefined, entonces success tenía que ser false. Por lo que sé, tu código podría ser un ejemplo de juguete y una refactorización similar no sería posible, pero sería negligente por no señalar esto, ya que simplifica considerablemente tanto la llamada como la implementación de login().

Enlace de Playground al código

Respuesta #2

Lo que quieres hacer no es posible. Cuando escribes una función con tipos genéricos, escribes los parámetros y los valores de retorno. Los tipos genéricos se resuelven/inferirán en el código donde se llama a la función y se verifica la consistencia de los tipos de los parámetros y del valor de retorno.

Dentro de la implementación, los tipos no se resuelven, por lo que permanecen como T y T extends true? {a : string } : never.

Lo mejor que podrías hacer es usar verificaciones de tipo fuera de la implementación de callback, y usar sobrecarga de funciones en lugar de tipo condicional, para hacer que el segundo parámetro sea opcional:

function login({ callback }: { callback: Callback }) {
    const success = !Math.floor(Math.random() * 2);
    if (success) {
        callback(success, { a: 'test' }); // OK
        callback(success); // Fails
    }
    else {
        callback(success, { a: 'test' }); // Fails
        callback(success); // OK
    }
}

interface Callback {
    (success: true, res: { a: string }): void;
    (success: false): void;
}

Área de juego de TypeScript