在 TypeScript 中定义不同泛型类型的数组
Posted
技术标签:
【中文标题】在 TypeScript 中定义不同泛型类型的数组【英文标题】:Defining an array of differing generic types in TypeScript 【发布时间】:2021-03-15 15:37:45 【问题描述】:interface Instruction
promise: Promise<unknown>,
callback?: ($html: JQuery, data: unknown ) => void
const arr: Instruction[] = [
promise: Promise.resolve( foo: 'bar' ), callback: ($html, data) => console.log(data.foo) ,
promise: Promise.resolve( bar: 'foo' ), callback: ($html, data) => console.log(data.bar)
];
鉴于上述情况,我希望 TypeScript 能够识别回调函数中的 data 参数与 Promise 的解析类型相同。
如果它是独立的,我可以这样做:
interface Instruction<T>
promise: Promise<T>,
callback?: ($html: JQuery, data: T) => void
但是我将如何定义数组,其中T
在每一行可能意味着不同的东西?
【问题讨论】:
在您的实际代码中,arr
会是一个长度和类型在编译时已知的不可变数组吗?
@Aplet123,在实际代码中,数组是在更大的类中构造的,使用push
。
【参考方案1】:
也许对你有帮助:
type JQuery = 'jquery'
interface Instruction<T>
promise: Promise<T>,
callback?: ($html: JQuery, data: T) => void
const builder = <T,>(arg: T): Instruction<T> => ( promise: Promise.resolve(arg), callback: ($html, data) => data )
const arr = [
builder( foo: 'bar' ),
builder( bar: 'foo' ),
];
如果这个数组是可变的,事情就会变得复杂。
更新
您必须添加额外的逗号<T,>
,因为没有,
TS 认为您正在尝试使用某种react jsx
语法。
你应该只用箭头函数来使用这个技巧。它与function
一样工作。
代替extra comma
,您可以使用<T extends unknown>
或<T extends >
【讨论】:
谢谢@capain-yossarian!我没有想到要像这样包装指令。你能解释一下<T,>
发生了什么吗?我搜索了互联网,但无法理解。这是某种捷径吗?【参考方案2】:
这实际上是 existential generic types 的规范用例,TypeScript 不直接支持它们(大多数具有泛型的语言也不直接支持它们,因此这不是 TypeScript 的特殊缺点)。有一个开放的功能请求,microsoft/TypeScript#14466,要求这样做,但它不是 TS4.1 语言的一部分。
TypeScript 中的泛型是“通用的”,这意味着当我说class Foo<T> ...
时,我的意思是它适用于所有 可能的类型参数T
。这让Foo<T>
的消费者 指定T
的值并用它做他们想做的事,而Foo<T>
的提供者 需要允许所有可能性。
像您试图描述的那样的异构集合需要“存在”泛型。从某种意义上说,您希望interface Instruction<exists T> ...
意味着有一个类型参数T
可以工作。这意味着Instruction
的provider 可以指定T
的值并用它做他们想做的事情,而Instruction
的consumer 需要允许所有的可能性。
有关普遍与存在量化的泛型的更多信息,请参阅this SO question and answer。
虽然 TypeScript 中没有 direct 支持存在,但有 indirect 支持。普遍和存在之间的区别与谁在看类型有关。如果你转换生产者和消费者的角色,你就会得到类似存在主义的行为。这可以通过回调来完成。所以存在词可以在 TypeScript 中编码。
让我们看看如何为Instruction
做这件事。首先,让我们将Instruction
定义为通用泛型,即您提到的“独立”版本(我将删除此代码中的JQuery
依赖项):
interface Instruction<T>
promise: Promise<T>,
callback?: (data: T) => void
这是存在编码,SomeInstruction
:
type SomeInstruction = <R>(cb: <T>(instruction: Instruction<T>) => R) => R;
SomeInstruction
是一个函数,它调用一个函数,该函数接受任何 T
的 Instruction<T>
并返回结果。请注意SomeInstruction
本身如何不再依赖于T
。您可能想知道如何获得 SomeInstruction
,但这也相当简单。让我们创建一个辅助函数,将任何Instruction<T>
转换为SomeInstruction
:
const someInstruction = <T,>(i: Instruction<T>): SomeInstruction => cb => cb(i);
最后我们可以制作你的异构集合:
const arr: SomeInstruction[] = [
someInstruction(
promise: Promise.resolve( foo: 'bar' ),
callback: (data) => console.log(data.foo)
),
someInstruction(
promise: Promise.resolve( bar: 'foo' ),
callback: (data) => console.log(data.bar)
)
]
根据需要进行所有类型检查。
实际上使用SomeInstruction
比使用Instruction<T>
更复杂一些,因为它需要一个回调。但这并不可怕,并且再次允许T
类型参数以消费者不知道实际T
类型是什么的方式出现,因此必须将其视为任何可能的T
:
// writing out T for explicitness here
arr.forEach(someInstruction => someInstruction(<T,>(i: Instruction<T>) =>
i.promise.then(i.callback); // works
))
// but it is not necessary:
arr.forEach(someInstruction => someInstruction(i =>
i.promise.then(i.callback); // works
))
不错。
还有其他解决方法,但存在主义才是你真正想要的。为了完整起见,以下是一些可能的解决方法,我将提及但未实施:
放弃类型安全并使用any
或unknown
并使用类型断言来取回您想要的类型。布莱克。
使用mapped tuple type 将[T1, T2, T3]
之类的类型转换为相应的[Instruction<T1>, Instruction<T2>, Instruction<T3>]
类型。不过,这不适用于 push()
,因此您也需要以某种方式解决这个问题。
重构 Instruction
以免需要/公开泛型。无论消费者打算用Instruction<T>
做什么,它都必须独立于T
(例如,我可以写i.promise.then(i.callback)
,但我不能做很多其他事情,对吧?),所以创建一个Instruction
通用函数这需要一个有效的 promise-and-callback 对来创建,并返回具有您需要的任何功能的非泛型的东西,仅此而已。从某种意义上说,这是一个“精简”的存在,它不允许消费者单独访问内部的 promise-callback 对。
Playground link to code
【讨论】:
类型系统聂文章! @jcalz:这太棒了,谢谢。我想我明白这里发生了什么,但我正在努力使用这种语法<T,>
。为什么是逗号?它似乎声明了一个未使用的类型参数,但我不明白为什么。
尾随逗号不引入新参数。这只是为了阻止编译器在您使用 tsx/jsx 的情况下生气。 (在 tsx/jsx 模式下,编译器将该位置的 <T>
视为标记并抱怨)。如果为您编译,您可以删除逗号。
哇哦,好吧!我无法决定我现在是否感到或多或少愚蠢。谢谢!以上是关于在 TypeScript 中定义不同泛型类型的数组的主要内容,如果未能解决你的问题,请参考以下文章