打字稿:将元组映射到联合类型似乎在嵌套对象中不起作用

Typescript: Mapping Tuples to Union types doesn't seem work inside nested objects

我在 Typescript 中有一个 Vue 项目,我 运行 遇到了一个关于将元组对象映射到联合对象类型的问题。

关于一些上下文,我正在研究后端端点的预期响应类型。目前,此端点接收 2 个值:一个枚举和一个字符串。根据枚举,响应对象将发生变化。

这是当前的实现:

const validations = {
  email: ['isValid', 'isAvaliable'],
  password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
  iban: ['isValid', 'isRegistered']
} as const

type ValidationsMap = {
  [T in keyof typeof validations]: typeof validations[T][number]
}

function validate<T extends keyof ValidationsMap>(type: T, value: string): Record<ValidationsMap[T], boolean> {
  // Do something
}

现在 Backend 端点将多接收一个参数,响应对象也将依赖于它。

这是我试过的方法,但它不起作用:

const validations = {
  portal: {
    email: ['isValid', 'isAvaliable'],
    password: ['isMinLengthValid', 'hasUppercase', 'hasLowercase', 'hasSpecialChars', 'hasNumbers'],
  },
  payment: {
    email: ['isValid'],
    iban: ['isValid', 'isRegistered']
  }
} as const

type ValidationsMap = {
  [S in keyof typeof validations]: {
    [T in keyof typeof validations[S]]: typeof validations[S][T][number] // Error: Type 'number' cannot be used to index type...
  }
}

function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(service: S, type: T, value: string): Record<ValidationsMap[S][T], boolean> {
  // Do something
}

有谁知道为什么这不起作用?

我认为将元组映射到联合的深度可能存在限制,但事实并非如此。

这可以正常工作:

type PortalEmailValidation = typeof validations['portal']['email'][number]

这似乎是 TypeScript 中的一个错误,如 microsoft/TypeScript#27709. The type checker apparently doesn't properly track the constraints for deep index access types when the keys are generic 中所述。这个问题已经开放了很长时间,没有任何进展的迹象,所以现在我们所能做的就是解决它。


当编译器不接受 T[K] 形式的索引访问时,一种方法是使用 conditional type inference, like T extends Record<K, infer V> ? V : never (using the Record<K, V> utility type)。如果 KT 的 (non-optional) 键,那么 T 将被视为某些 VRecord<K, V>,我们推断.

所以我们可以写 typeof validations[S][T] extends Record<number, infer V> ? V : never 而不是 typeof validations[S][T][number]。或者等价地:

type ValidationsMap = {
  [S in keyof typeof validations]: {
    [T in keyof typeof validations[S]]:
    typeof validations[S][T] extends { [k: number]: infer V } ? V : never
  }
}

另一种方法是显式添加回缺失的约束。如果你有一个 知道的类型 A 可以分配给另一个类型 B 但编译器不知道这一点,那么你可以替换 AExtract<A, B> (使用 the Extract<T, U> utility type),编译器将接受它。它知道 Extract<A, B> 可以分配给 B。假设您关于 A 可分配给 B 的说法是正确的,那么 Extract<A, B> 的计算结果将只是 A.

所以如果ValidationsMap[S][T]可以赋值给string但是编译器看不到,我们可以写成Extract<ValidationsMap[S][T], string>

function validate<S extends keyof ValidationsMap, T extends keyof ValidationsMap[S]>(
  service: S, type: T, value: string
): Record<Extract<ValidationsMap[S][T], string>, boolean> { // okay
  return null!
}

Playground link to code