typescript - cómo tipar los parámetros de una función como un valor de objeto basado en la clave
Estoy teniendo dificultades para escribir el tipo de configArgs. ¿Alguien puede ayudar?
const path = {
A: 'home',
B: 'support',
C: 'contact'
} as const
type B = unknown
const B: B = {};
const config = {
[path.A]: () => null,
[path.B]: (): B => B,
[path.C]: ({
active,
}: {
active: boolean
}) => B,
} as const
type Config = typeof config
type ConfigArgs = {
[P in keyof Config]: Config[P] extends (a: infer A) => any ? A : never // this is not working
}
function getConfig<T extends A>(
path: A,
args?: ConfigArgs[R]
): B | null {
return config[path](args)
}
Esperaría ver el tipo de argumentos correctos cada vez que llame a la función getConfig con la ruta.
getConfig("pathA") // no type errors
getConfig("pathC", {active:true}) // no type errors
getConfig("pathC", {id:"xxx"}) // has type errors, as parameters are wrong
getConfig("pathC") // has type errors, as function needs params.
getConfig("pathA", {active:true}) // has type errors, cause the function does not expect any params
Respuesta #1
Si todo lo que necesitas respaldar es la tipificación fuerte para los llamadores de getConfig(), entonces puedes hacerlo genérico en el tipo K del argumento path, y usar los tipos de utilidad Parameters<T> y ReturnType<T> para representar la relación de entrada/salida:
declare function getConfig<K extends keyof Config>(
path: K,
...args: Parameters<Config[K]>
): ReturnType<Config[K]>;
lo cual funciona como se espera:
getConfig(path.A) // okay
getConfig(path.B, { id: "xxx" }) // error, expected 1 arg but got 2
getConfig(path.C) // error // error, expected 2 args but got 1
getConfig(path.C, { active: true }) // okay
getConfig(path.C, { id: "xxx" }) // error, {id: string} is not {active: boolean}
Sin embargo, descubrirás que el compilador no podrá verificar que la implementación de esta función sea correcta:
function getConfig<K extends keyof Config>(
path: K,
...args: Parameters<Config[K]>
): ReturnType<Config[K]> {
return config[path](...args); // error!
// ---------------> ~~~~~~~ A spread argument must have a tuple type
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ <-- unknown is not assignable to type...
}
Esa es una limitación actual de TypeScript; los tipos condicionales genéricos como Parameters<Config[K]> y ReturnType<Config[K]> son esencialmente opacos para el compilador. Aún puedes usar estos tipos, pero necesitarás utilizar aserciones de tipo en la implementación para suprimir errores, y debes asegurarte de haber hecho el trabajo correctamente porque el compilador no puede ayudar aquí:
function getConfig<K extends keyof Config>(
path: K,
...args: Parameters<Config[K]>
) {
return (config as any)[path](...args) as ReturnType<Config[K]>
// ^^^^^^ <-- assert ---> ^^^^^^^^^^^^^^^^^^^^^^^^
}
Si deseas que el compilador siga la lógica genérica, necesitarás refactorizar usando accesos indexados en tipos mapeados genéricos como se describe en microsoft/TypeScript#47109:
type ConfigArgs = { [K in keyof Config]: Parameters<Config[K]> }
type ConfigRet = { [K in keyof Config]: ReturnType<Config[K]> }
const _config: { [K in keyof Config]:
(...args: ConfigArgs[K]) => ConfigRet[K]
} = config;
function getConfig<K extends keyof Config>(
path: K,
...args: ConfigArgs[K]
) {
return _config[path](...args) // okay, seen as ConfigRet[K]
}
Aquí definimos tanto ConfigArgs como ConfigRet como un tipo mapeado sobre Config, y luego definimos la variable _config como un tipo mapeado de funciones que toman ConfigArgs[K] y devuelven ConfigRet[K] para cada clave K. El compilador nos permite asignar config a esta variable, porque es capaz de expandir ese tipo mapeado en el mismo tipo específico que config.
Y ahora se ve que la función getConfig() está realizando una operación en _config[path], un valor de tipo (...args: ConfigArgs[K]) => ConfigRet[K], y por lo tanto acepta un argumento rest de tipo ConfigArgs[K] y produce una salida de tipo ConfigRet[K].
Así que todo sigue funcionando desde el lado del llamador, y la implementación también está verificada por tipo.
Tenga en cuenta que conceptualmente no hay diferencia entre typeof config y typeof _config, pero el compilador ve typeof _config como representando la relación genérica entre entrada y salida de una manera que no puede ver para typeof config.
Enlace de Playground al código
