简体   繁体   English

如何在TypeScript中定义泛型数组?

[英]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.transformerTransform.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)

Explanation 说明

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM