Home > Mobile >  how to type parameters of a function as an object value based on the key
how to type parameters of a function as an object value based on the key

Time:01-12

I am having difficulties in writing the type of configArgs Can anybody help?

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) 
}

I would expect to see the correct arguments type whenever i call the getConfig function with the path.

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

CodePudding user response:

If all you need to support is strong typing for the callers of getConfig(), then you can make it generic in the type K of the path argument, and use the Parameters<T> and the ReturnType<T> utility types to represent the input/output relationship:

declare function getConfig<K extends keyof Config>(
  path: K,
  ...args: Parameters<Config[K]>
): ReturnType<Config[K]>;

which works as expected:

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}

However, you'll find that the compiler won't be able to verify that the implementation of this function is correct:

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...
}

That's a current limitation of TypeScript; generic conditional types like Parameters<Config[K]> and ReturnType<Config[K]> are essentially opaque to the compiler. You can still use these types, but you'll need to use type assertions in the implementation to suppress errors, and take care that you've done the work correctly because the compiler can't help here:

function getConfig<K extends keyof Config>(
  path: K,
  ...args: Parameters<Config[K]>
) {
  return (config as any)[path](...args) as ReturnType<Config[K]>
  //             ^^^^^^ <-- assert ---> ^^^^^^^^^^^^^^^^^^^^^^^^
}

If you want the compiler to follow the generic logic, you will need to refactor to using indexed accesses on generic mapped types as described in 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]
}

Here we define both ConfigArgs and ConfigRet as a mapped type on Config, and then define the _config variable as a mapped type of functions taking ConfigArgs[K] and returning ConfigRet[K] for each key K. The compiler lets us assign config to this variable, because it's able to expand that mapped type into the same specific type as config.

And now the getConfig() function is seen is performing an operation on _config[path], a value of type (...args: ConfigArgs[K]) => ConfigRet[K], and therefore it accepts a rest argument of type ConfigArgs[K] and produces an output of type ConfigRet[K].

So everything still works from the caller's side, and the implementation is also type checked.

Note that there is conceptually no difference between typeof config and typeof _config, but the compiler sees typeof _config as representing the generic relationship between input and output in a way it cannot see for typeof config.

Playground link to code

  • Related