网站搜索

typescript - 数组中可能有泛型约束吗?


是否可以让 TypeScript 使用第一个参数来限制第二个索引?

例如:

type Data = {
  FOO: number
  BAR: string
}

type DataKeys = keyof Data

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]

// I want both of these array to fail because the 2nd argument's type
// is determined by the first argument
const arr1: SpecialArray = ['FOO', 'wrong'] // 2nd argument should be `number`
const arr2: SpecialArray = ['BAR', 666] // 2nd argument should be `string`

// This works well with functions
const specialFunction = <T extends DataKeys = DataKeys>(key: T, value: Data[T]) => {
  return
}
specialFunction('FOO', 'wrong')
specialFunction('BAR', 666)

游乐场链接

答案#1

是也不是。

在这种情况下:

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]
const arr1: SpecialArray = ['FOO', 'wrong'] // no error

你省略了通用参数,因此使用了默认值。由于默认类型 DataKeys 包含所有键,因此结果值类型是 number | string。

然而,所有泛型参数必须分配一个类型,所以如果你只是移除默认值,是行不通的:

type SpecialArray<T extends DataKeys> = [T, Data[T]]
const arr1: SpecialArray = ['FOO', 'wrong'] // error
// Generic type 'SpecialArray' requires 1 type argument(s).(2314)

如果你指定泛型参数,它确实有效:

// errors as expected
const arr1: SpecialArray<'FOO'> = ['FOO', 'wrong'] // 2nd argument should be `number`
const arr2: SpecialArray<'BAR'> = ['BAR', 666] // 2nd argument should be `string`

但那并不完全好看。

遗憾的是,你无法仅从 TypeScript 中的简单赋值推断出泛型参数。

对此的典型解决方法是使用一个函数来为你创建对象。正如你所注意到的,函数对此处理得更好。

function makeSpecialArray<
  T extends DataKeys
>(key: T, value: Data[T]): SpecialArray<T> {
  return [key, value]
}

// these are type errors, and the objects they create are SpecialArray<SomeKey>
const arr1 = makeSpecialArray('FOO', 'wrong') // 2nd argument should be `number`
const arr2 = makeSpecialArray('BAR', 666) // 2nd argument should be `string`

操场

答案#2

在这种情况下,你实际上只希望 SpecialArray 是一个特定的联合类型,形式为 ["FOO", number] | ["BAR", string]。 这满足了你所有的需求:

type SpecialArray = ["FOO", number] | ["BAR", string]
const arr1: SpecialArray = ['FOO', 'wrong'] // error
const arr2: SpecialArray = ['BAR', 666] // error
const arr3: SpecialArray = ['FOO', 123] // okay
const arr4: SpecialArray = ['BAR', 'right'] // okay

所以这里的问题只是“我如何从 Data 计算 SpecialArray?”


没有理由让 SpecialArray<T> 在 T 上保持泛型,但如果你确实让它在 T 上泛型化,你会希望将类型操作分布到 T 中的任何联合类型上。你当前的版本只是生成了一个单一的元组类型:

type SpecialArray<T extends DataKeys = DataKeys> = [T, Data[T]]
type Test = SpecialArray;
// type Test = [keyof Data, string | number]

你可以使用分配条件类型将类型操作分配到联合类型上:

type SpecialArray<T extends DataKeys = DataKeys> = 
   T extends any ? [T, Data[T]] : never;

type Test = SpecialArray; 
// type Test = ["FOO", number] | ["BAR", string]

T extends any ? XXX : never 看起来像一个产生 XXX 的无操作,但它具有将 DataKeys 拆分为 "FOO" 和 "BAR" 的预期效果,分别计算每个元组类型,然后再将它们联合起来。


但正如我所说,你不需要让 SpecialArray 是泛型;你可以直接通过被称为“分配对象类型”(在 ms/TS#47109 中提出)的方法从 Data 计算它,在这种方法中,你对 DataKeys 中的每个键创建一个映射类型,其中对应的属性是你想要的元组类型,然后立即使用 DataKeys 对其进行索引,以获取所有属性的联合类型。像这样:

type SpecialArray = { [K in DataKeys]: [K, Data[K]] }[DataKeys];
// type SpecialArray = ["FOO", number] | ["BAR", string]

所以这可能是我在这里的建议;将 SpecialArray 制作成一个特定的(也就是说,非泛型的)分发对象类型。并且如果 Data 发生更新,SpecialArray 将会自动更新:

type Data = {
  FOO: number
  BAR: string
  BAZ: boolean
}

// type SpecialArray = ["FOO", number] | ["BAR", string] | ["BAZ", boolean];

代码的 Playground 链接