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
.