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

use*_*447 4 generics typescript

假设我有一个如下通用接口:

interface Transform<ArgType> {
    transformer: (input: string, arg: ArgType) => string;
    arg: ArgType;
}
Run Code Online (Sandbox Code Playgroud)

然后我想将这些数组Transform应用于一个string。我如何定义这样的数组,Transform以使其<ArgType>Transform.transformer和中都等效Transform.arg?我想写这样的东西:

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;
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,const transforms为了使类型系统验证数组中的每个项目均满足通用Transform<ArgType>接口,我将定义为哪种类型?

jca*_*alz 6

(以下使用TS 3.0)

如果TypeScript直接支持存在类型,我会告诉您使用它们。存在类型意味着“我所知道的就是该类型存在,但我不知道或不在乎它是什么”。然后,您的transforms参数具有类似的类型Array< exists A. Transform<A> >,表示“ Transform<A>用于某些 事物的数组A”。有一种建议允许在语言中使用这些类型,但是很少有人支持。

您可以“放弃”并仅使用Array<Transform<any>>,它可以工作,但无法捕获如下不一致的情况:

applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // no error
Run Code Online (Sandbox Code Playgroud)

但是正如您所说的,即使没有存在性类型,您也希望实现一致性。幸运的是,有一些变通办法,它们的复杂程度各不相同。这是一个:


让我们声明一个带a的类型函数T,如果a Transform<A>对于some A,它将返回unknown(匹配每个值的新的顶级类型 ...因此unknown & T等于Tall T),否则返回never(不匹配任何值的底部类型)。 ..所以never & T等于never所有的T):

type VerifyTransform<T> = unknown extends
  (T extends { transformer: (input: string, arg: infer A) => string } ?
    T extends { arg: A } ? never : unknown : unknown
  ) ? never : unknown
Run Code Online (Sandbox Code Playgroud)

它使用条件类型来计算。这个想法是transformer要找出来A,然后确保arg与之兼容A

现在我们可以将其键入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;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们看到它正在工作:

applyTransforms("hey", transforms); // okay
Run Code Online (Sandbox Code Playgroud)

如果传递不一致的内容,则会出现错误:

applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // error
Run Code Online (Sandbox Code Playgroud)

该错误不是特别说明:“ [ts] Argument of type '{ transformer: (input: string, arg: number) => string; arg: string; }[]' is not assignable to parameter of type 'never'.”,但至少是一个错误。


或者,您可能会意识到,如果您要做的只是传递argtransformer,则可以使您的类似于存在的SomeTransform类型:

interface SomeTransform {
  transformerWithArg: (input: string) => string;
}
Run Code Online (Sandbox Code Playgroud)

SomeTransformTransform<A>您想要的任何东西中获取一个:

const makeSome = <A>(transform: Transform<A>): SomeTransform => ({
  transformerWithArg: (input: string) => transform.transformer(input, transform.arg)
});
Run Code Online (Sandbox Code Playgroud)

然后接受SomeTransform代替的数组:

function applySomeTransforms(input: string, transforms: SomeTransform[]): string {
  for (const someTransform of transforms) {
    input = someTransform.transformerWithArg(input);
  }

  return input;
}
Run Code Online (Sandbox Code Playgroud)

查看是否有效:

const someTransforms = [
  makeSome({
    transformer: append,
    arg: " END"
  }),
  makeSome({
    transformer: repeat,
    arg: 4
  }),
];

applySomeTransforms("h", someTransforms);
Run Code Online (Sandbox Code Playgroud)

如果您尝试不一致地执行此操作,则:

makeSome({transformer: repeat, arg: "oops"}); // error
Run Code Online (Sandbox Code Playgroud)

您会得到一个更合理的错误:“ Types of parameters 'arg' and 'arg' are incompatible. Type 'string' is not assignable to type 'number'.


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

  • @TitianCernicova-Dragomir:这种简化可能有效……但我遇到了一个问题,即`infer A` 最终会推断出类型的交集(因此是两步过程),或者union 工作使整个事情工作,而我们需要所有类型工作(因此永远不会未知并再次返回的东西使一个失败给所有失败)。我希望这失败:`applyTransforms("hey", [{transformer: repeat, arg: 1}, {transformer: repeat, arg: "oops"}]);` (2认同)

小智 6

您可以使用通用元组其余参数(在 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)
Run Code Online (Sandbox Code Playgroud)

解释

Typescript 具有非常强大的类型推断,但通常会选择最松散的类型。在这种情况下,您需要强制它将您的转换视为一个元组,以便每个元素都有自己的类型,然后让推理完成剩下的工作。

我用映射类型做到了这一点,一个小问题是 Typescript 将使用所有元组键(例如“长度”),而不仅仅是数字键。你只需要强制它只映射数字。因此条件: T[P] extends T[number]