网站搜索

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 链接