Búsqueda de sitios web

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