如何通过TypeScript中的类型描述元组数组中元组元素之间的关系?

时间:2019-05-17 14:13:00

标签: typescript generics generic-programming typescript-generics

我想通过TypeScript中的类型来描述元组数组中元组元素之间的关系。

这可能吗?

declare const str: string;
declare const num: number;
function acceptString(str: string) { }
function acceptNumber(num: number) { }

const arr /*: ??? */ = [
    [str, acceptString],
    [num, acceptNumber],
    [str, acceptNumber], // should be error
];

for (const pair of arr) {
    const [arg, func] = pair;
    func(arg); // should be no error
}

TypeScript Playground link

实际示例:TypeScript Playground link

1 个答案:

答案 0 :(得分:2)

您基本上是在问我一直在叫correlated record types的东西,目前TypeScript中没有直接的支持。即使在您可以说服编译器在创建此类记录时捕获错误的情况下,在使用一个记录时也无法真正验证类型安全。

实现此类类型的一种方法是使用existentially quantified generics(TypeScript does not currently support directly)。如果是这样,您可以将数组描述为:

type MyRecord<T> = [T, (arg: T)=>void];
type SomeMyRecord = <exists T> MyRecord<T>; // this is not valid syntax
type ArrayOfMyRecords = Array<SomeMyRecord>;

下一个最好的办法是允许ArrayOfMyRecords本身是一个泛型类型,其中数组的每个元素都以类似的T值强类型化,并使用一个辅助函数来推断类型:

type MyRecord<T> = [T, (arg: T) => void];

const asMyRecordArray = <A extends any[]>(
  a: { [I in keyof A]: MyRecord<A[I]> } | []
) => a as { [I in keyof A]: MyRecord<A[I]> };

这使用infererence from mapped typesmapped tuples。让我们看看它的作用:

const arr = asMyRecordArray([
  [str, acceptString],
  [num, acceptNumber],
  [str, acceptNumber] // error
]);
// inferred type of arr:
// const arr:  [
//   [string, (arg: string) => void], 
//   [number, (arg: number) => void], 
//   [string, (arg: string) => void]
// ]

让我们解决这个问题:

const arr = asMyRecordArray([
  [str, acceptString],
  [num, acceptNumber],
  [str, acceptString] 
]);
// inferred type of arr:
// const arr:  [
//   [string, (arg: string) => void], 
//   [number, (arg: number) => void], 
//   [string, (arg: string) => void]
// ]

因此可以很好地定义arr。但是,现在看看迭代时会发生什么:

// TS3.3+ behavior
for (const pair of arr) {
  const [arg, func] = pair; 
  func(arg); // still error!
}

在这里,缺乏对相关记录的支持使您心痛。在TypeScript 3.3中,添加了对调用unions of function types的支持,但是该支持并未解决此问题,即:编译器将func视为函数的 union ,这完全是不相关,类型为arg。当您调用它时,编译器决定它只能安全​​地接受类型为string & number的参数,arg不是(也不是任何实际值,因为string & number会折叠为{{1} }。

因此,如果您采用这种方式,则会发现需要使用type assertion来使编译器平静下来:

never

一个人可能会认为这是您可以做的最好的事情,然后把它留在那里。


现在,有一种方法可以在TypeScript中编码存在类型,但是它涉及类似for (const pair of arr) { const [arg, func] = pair as MyRecord<string | number>; func(arg); // no error now func(12345); // no error here either, so not safe } 的控件反转。但是,在走那条路之前,请问自己:如果您不知道Promise,您将如何实际使用MyRecord<T> ?您可以做的唯一合理的事情是将其第一个元素称为第二个元素。如果是这样,您可以给出一种更具体的方法,只需做到这一点而无需跟踪T

T

您的真实示例可以进行类似的修改:

type MyRecord<T> = [T, (arg: T) => void];
type MyUsefulRecord<T> = MyRecord<T> & { callFuncWithArg(): void };

function makeUseful<T>(arg: MyRecord<T>): MyUsefulRecord<T> {
    return Object.assign(arg, { callFuncWithArg: () => arg[1](arg[0]) });
}

const asMyUsefulRecordArray = <A extends any[]>(
    a: { [I in keyof A]: MyUsefulRecord<A[I]> } | []
) => a as { [I in keyof A]: MyUsefulRecord<A[I]> };

const arr = asMyUsefulRecordArray([
    makeUseful([str, acceptString]),
    makeUseful([num, acceptNumber]),
    makeUseful([str, acceptString])
]);

for (const pair of arr) {
    pair.callFuncWithArg(); // okay!
}

在TypeScript中模拟存在类型与上面类似,不同之处在于它允许您对function creatify<T, U>(arg: [new () => T, new (x: T) => U]) { return Object.assign(arg, { create: () => new arg[1](new arg[0]()) }); } const map = { [Type.Value1]: creatify([Store1, Form1]), [Type.Value2]: creatify([Store2, Form2]) }; function createForm(type: Type) { return map[type].create(); } 进行绝对任何操作,如果您不知道MyRecord<T>则可以做到。由于在大多数情况下,这只是一小部分操作,所以直接直接支持这些操作通常会更容易。


好的,希望能有所帮助。祝你好运!