[英]How do you define an array of generics in TypeScript?
Let's say I have a generic interface like the following: 假设我有一个如下通用界面:
interface Transform<ArgType> {
transformer: (input: string, arg: ArgType) => string;
arg: ArgType;
}
And then I want to apply an array of these Transform
to a string
. 然后我想将这些
Transform
的数组应用于string
。 How do I define this array of Transform
such that it validates that <ArgType>
is equivalent in both Transform.transformer
and Transform.arg
? 如何定义此
Transform
数组,以便验证<ArgType>
在Transform.transformer
和Transform.arg
是否相同? I'd like to write something like this: 我想写这样的东西:
function append(input: string, arg: string): string {
return input.concat(arg);
}
function repeat(input: string, arg: number): string {
return input.repeat(arg);
}
const transforms = [
{
transformer: append,
arg: " END"
},
{
transformer: repeat,
arg: 4
},
];
function applyTransforms(input: string, transforms: \*what type goes here?*\): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
In this example, what type do I define const transforms
as in order for the type system to validate that each item in the array satisfies the generic Transform<ArgType>
interface? 在这个例子中,我定义了
const transforms
的类型,以便类型系统验证数组中的每个项是否满足泛型Transform<ArgType>
接口?
(Using TS 3.0 in the following) (以下使用TS 3.0)
If TypeScript directly supported existential types , I'd tell you to use them. 如果TypeScript直接支持存在类型 ,我会告诉你使用它们。 An existential type means something like "all I know is that the type exists, but I don't know or care what it is."
存在主义类型意味着“我所知道的是类型存在,但我不知道或不关心它是什么。” Then your
transforms
parameter have a type like Array< exists A. Transform<A> >
, meaning "an array of things that are Transform<A>
for some A
". 然后你的
transforms
参数有一个像Array< exists A. Transform<A> >
这样的类型,意思是“对于某些 A
来说是一个Transform<A>
的数组”。 There is a suggestion to allow these types in the language, but few languages support this so who knows. 有一个建议允许这些类型的语言,但很少有语言支持这个谁知道。
You could "give up" and just use Array<Transform<any>>
, which will work but fail to catch inconsistent cases like this: 您可以“放弃”并使用
Array<Transform<any>>
,这将起作用但无法捕获这样的不一致情况:
applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // no error
But as you said you're looking to enforce consistency, even in the absence of existential types. 但正如你所说,即使在没有存在类型的情况下,你也希望强化一致性。 Luckily, there are workarounds, with varying levels of complexity.
幸运的是,有一些变通方法具有不同程度的变通方法。 Here's one:
这是一个:
Let's declare a type function which takes a T
, and if it a Transform<A>
for some A
, it returns unknown
(the new top type which matches every value... so unknown & T
is equal to T
for all T
), otherwise it returns never
(the bottom type which matches no value... so never & T
is equal to never
for all T
): 让我们来声明一个类函数,它接受一个
T
的,如果它Transform<A>
对一些 A
,它返回unknown
(新的顶级型 ,其每一个值相匹配......所以unknown & T
等于T
所有T
)否则它会返回never
(不匹配任何值的底部类型 ......所以never & T
等于所有T
never
):
type VerifyTransform<T> = unknown extends
(T extends { transformer: (input: string, arg: infer A) => string } ?
T extends { arg: A } ? never : unknown : unknown
) ? never : unknown
It uses conditional types to calculate that. 它使用条件类型来计算它。 The idea is that it looks at
transformer
to figure out A
, and then makes sure that arg
is compatible with that A
. 这个想法是它看着
transformer
找出A
,然后确保arg
与那个A
兼容。
Now we can type applyTransforms
as a generic function which only accepts a transforms
parameter which matches an array whose elements of type T
match VerifyTransform<T>
: 现在我们可以输入
applyTransforms
作为泛型函数,它只接受一个transforms
参数,该参数匹配类型为T
的元素匹配VerifyTransform<T>
的数组:
function applyTransforms<T extends Transform<any>>(
input: string,
transforms: Array<T> & VerifyTransform<T>
): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
Here we see it working: 在这里,我们看到它工作:
applyTransforms("hey", transforms); // okay
If you pass in something inconsistent, you get an error: 如果传入不一致的内容,则会出现错误:
applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // error
The error isn't particularly illuminating: " [ts] Argument of type '{ transformer: (input: string, arg: number) => string; arg: string; }[]' is not assignable to parameter of type 'never'.
" but at least it's an error. 错误并不是特别有启发性:“
[ts] Argument of type '{ transformer: (input: string, arg: number) => string; arg: string; }[]' is not assignable to parameter of type 'never'.
“但至少这是一个错误。
Or, you could realize that if all you're doing is passing arg
to transformer
, you can make your existential-like SomeTransform
type like this: 或者,你可以意识到,如果你所做的只是将
arg
传递给transformer
,你可以像这样SomeTransform
类似于存在主义的SomeTransform
类型:
interface SomeTransform {
transformerWithArg: (input: string) => string;
}
and make a SomeTransform
from any Transform<A>
you want: 并根据您想要的任何
Transform<A>
制作SomeTransform
:
const makeSome = <A>(transform: Transform<A>): SomeTransform => ({
transformerWithArg: (input: string) => transform.transformer(input, transform.arg)
});
And then accept an array of SomeTransform
instead: 然后接受一个
SomeTransform
数组:
function applySomeTransforms(input: string, transforms: SomeTransform[]): string {
for (const someTransform of transforms) {
input = someTransform.transformerWithArg(input);
}
return input;
}
See if it works: 看看它是否有效:
const someTransforms = [
makeSome({
transformer: append,
arg: " END"
}),
makeSome({
transformer: repeat,
arg: 4
}),
];
applySomeTransforms("h", someTransforms);
And if you try to do it inconsistently: 如果你尝试不一致地做:
makeSome({transformer: repeat, arg: "oops"}); // error
you get an error which is more reasonable: " Types of parameters 'arg' and 'arg' are incompatible. Type 'string' is not assignable to type 'number'.
" 你得到一个更合理的错误:“
Types of parameters 'arg' and 'arg' are incompatible. Type 'string' is not assignable to type 'number'.
”
Okay, hope that helps. 好的,希望有所帮助。 Good luck.
祝好运。
You can do this using the generic tuple rest parameters (added in TS 3.0). 您可以使用通用元组休息参数(在TS 3.0中添加)来执行此操作。
type TransformRest<T extends any[]> = {
[P in keyof T]: T[P] extends T[number] ? Transform<T[P]> : never
}
function applyTransforms<T extends any[]>(input: string, ...transforms: TransformRest<T>): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
// Makes a tuple from it's arguments, otherwise typescript always types as array
function tuplify<TS extends any[]>(...args: TS) {
return args;
}
// Use like this:
const transforms = tuplify(
{
transformer: append,
arg: " END"
},
{
transformer: repeat,
arg: 4
},
);
//And call apply transforms like this:
applyTransforms("string", ...transforms)
//or like this:
applyTransforms("string", transform1, transform2)
Typescript has really powerful type inference, but usually chooses the loosest types it can. Typescript具有非常强大的类型推断,但通常选择最宽松的类型。 In this case you need to force it to think of your transforms as a Tuple so that each element has it's own type, and then let the inference do the rest.
在这种情况下,您需要强制它将您的变换视为元组,以便每个元素都有自己的类型,然后让推理完成剩下的工作。
I did this with mapped types, the one hiccup with this is that Typescript will use all the tuple keys (such as "length"), not just the numeric ones. 我用映射类型做了这个,对此的一个打嗝是Typescript将使用所有元组键(例如“length”),而不仅仅是数字键。 You just need to force it to only map the numeric ones.
你只需要强制它只映射数字。 Hence the condition:
T[P] extends T[number]
因此条件:
T[P] extends T[number]
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.