【问题标题】:How to describe the relationship between elements of tuple in array of tuples via types in TypeScript?如何通过 TypeScript 中的类型来描述元组数组中元组元素之间的关系?
【发布时间】:2019-10-04 21:25:09
【问题描述】:

我想通过 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

【问题讨论】:

  • 无法进行全面类型检查。 [str, acceptNumber] 可能会出现错误(需要函数和条件类型),但 for 仍然需要类型断言
  • @TitianCernicova-Dragomir 谢谢。这是 TypeScript 的基本限制还是可以在将来修复?
  • 除了获取数组的每个元素并以第一个元素作为参数调用其第二个元素之外,您打算如何处理这样的数组?
  • TS 需要跟踪arg 来自pair,所以func(arg) 是安全的,而func("") 之类的则不是。虽然有可能对此进行跟踪,但我认为我们不会在不久的将来看到这一点。
  • @jcalz 在这个例子中什么都没有。但现实世界的例子有点复杂(我添加了问题的链接)。我认为这两个例子是等价的。

标签: typescript generics generic-programming typescript-generics


【解决方案1】:

您基本上是在要求我一直在调用 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 视为完全不相关的函数的联合 类型为arg。当您调用它时,编译器决定它只能安全​​地接受 string &amp; number 类型的参数,而 arg 不是(也不是任何实际值,因为 string &amp; number 折叠为 never)。

所以如果你这样做,你会发现你需要一个type assertion 来让编译器平静下来:

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
}

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


现在,有一种方法可以在 TypeScript 中对存在类型进行编码,但它涉及到类似Promise 的控制反转。不过,在我们走这条路之前,先问问自己:当您不知道 T 时,您打算用 MyRecord&lt;T&gt; 实际做什么?你能做的唯一合理的事情就是用它的第二个元素来调用它的第一个元素。如果是这样,您可以提供一个更具体的方法,只这样做而不跟踪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!
}

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

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();
}

在 TypeScript 中模拟存在类型与上述类似,只是它允许您对 MyRecord&lt;T&gt; 执行任何操作,如果您不知道 T 可以执行此操作。由于在大多数情况下这是一小组操作,因此直接支持这些操作通常更容易。


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

【讨论】:

    猜你喜欢
    • 2019-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-18
    • 2018-07-20
    • 2020-04-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多