typescript - 如何根据键将函数的参数类型定义为对象的值
我在编写 configArgs 类型时遇到了困难。有人可以帮忙吗?
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)
}
每当我使用路径调用 getConfig 函数时,我希望看到正确的参数类型。
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
答案#1
如果你只需要为 getConfig() 的 调用者 提供强类型支持,那么你可以在 path 参数的类型 K 上使用泛型,并使用 Parameters<T> 和 ReturnType<T> 工具类型来表示输入/输出关系:
declare function getConfig<K extends keyof Config>(
path: K,
...args: Parameters<Config[K]>
): ReturnType<Config[K]>;
如预期般工作:
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}
然而,你会发现编译器无法验证该函数的实现是否正确:
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...
}
这是 TypeScript 当前的一个限制;像 Parameters<Config[K]> 和 ReturnType<Config[K]> 这样的泛型条件类型对于编译器来说本质上是不可见的。你仍然可以使用这些类型,但在实现中需要使用类型断言来抑制错误,并且要确保你正确地完成了这项工作,因为编译器在这里无法提供帮助:
function getConfig<K extends keyof Config>(
path: K,
...args: Parameters<Config[K]>
) {
return (config as any)[path](...args) as ReturnType<Config[K]>
// ^^^^^^ <-- assert ---> ^^^^^^^^^^^^^^^^^^^^^^^^
}
如果你希望编译器遵循泛型逻辑,你将需要重构为使用在 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]
}
在这里,我们将 ConfigArgs 和 ConfigRet 都定义为 Config 上的映射类型,然后将 _config 变量定义为一个函数映射类型,该函数对每个键 K 接受 ConfigArgs[K] 并返回 ConfigRet[K]。编译器允许我们将 config 分配给该变量,因为它能够将该映射类型展开为与 config 相同的具体类型。
现在可以看到,getConfig() 函数正在对 _config[path] 执行操作,该值的类型为 (...args: ConfigArgs[K]) => ConfigRet[K],因此它接受类型为 ConfigArgs[K] 的剩余参数,并产生类型为 ConfigRet[K] 的输出。
所以从调用方的角度来看一切仍然可以工作,并且实现也经过了类型检查。
请注意,从概念上讲,typeof config 和 typeof _config 没有区别,但编译器将 typeof _config 视为以一种它无法在 typeof config 上看到的方式表示输入和输出之间的泛型关系。
代码的 Playground 链接
