TypeScript 中的通用 class 转换

Generic class transformation in TypeScript

我正在做一个项目,我需要实例化特定 class 的多个副本,但参数不同。我正在创建一个实用函数 withAlteredConstructorArgs 来帮助我解决这个问题,但我在正确设置类型时遇到了问题。

这是我想如何使用此实用程序的示例:

const createCycler = withAlteredConstructorArgs(
  Cycler,
  function reverseElementArgs<T>(elements: Array<T>): [Array<T>] {
    return [elements.reverse()];
  }
);

const cycler = createCycler(['foo', 'bar', 'baz', 'quux']);

console.log(cycler.get(), cycler.get());
// > should log 'quux', 'baz'

我编写的函数按预期工作,但没有按我预期的方式进行类型检查。我创建了一个 TypeScript Playground 来充分演示,但这里是实现:

function withAlteredConstructorArgs<Args extends Array<any>, T>(
  c: { new(...args: Args): T },
  fn: (...args: Args) => Args
): (...args: Args) => T {
  return (...args: Args) => new c(...fn(...args))
}

我认识到我没有足够地限制 T,这就是我收到错误消息的原因:'any[]' is assignable to the constraint of type 'Elements', but 'Elements' could be instantiated with a different subtype of constraint 'any[]'我了解此错误并已在 之前修复它,但不是与 class 构造函数参数相关的问题。 正确约束 T 的构造函数参数的正确语法是什么?

我认识到我可以通过创建一个工厂函数并从问题陈述中完全删除构造函数来解决这个问题,但我觉得理解这种方法总体上会更好地提高我对 TypeScript 的理解。另外,我已经使用工厂方法 similar issues,所以看起来构造函数方法无论如何都是可行的方法。

我能够通过以下更改让您的代码编译并生成预期的输出:

  • 正在将 Cycler 的定义从 class Cycler<Elements extends Array<any>> 更改为 class Cycler<E>
  • Cycler 的构造函数的签名从 constructor(private elements: Elements) 更改为 constructor(private elements: E[])

TS Playground

What is the correct syntax for properly constraining the constructor parameters for T?

我以为我根据你写的代码理解了你的问题直到我读了that,这让我不太确定您希望哪个参数成为创建约束的类型信息的提供者以及您希望哪个参数受到约束。

我将根据您编写代码的方式做出回应:您希望构造函数的参数作为 first 参数提供 Args,并让回调函数接受该类型的参数。

一个潜在的混淆点可能是多维数组:Args 是一个参数数组, 你的 class 构造函数 Cycler 接受数组作为其 第一个也是唯一的 参数(因此,数组中的数组)。

这里另一个潜在的混淆点是您似乎想要更高级的类型,但是 that's not available in TypeScript(至少)。似乎您希望 createCycler 是通用的(它不在您的示例中),并且您可以通过对其使用通用类型断言来使其通用。 (在其他情况下,您可以使用通用类型 annotation,但我认为这在这里不起作用。)

TS Playground

// Define the function:

type Fn<
  Params extends unknown[] = any[],
  Result = any,
> = (...params: Params) => Result;

type Ctor<
  Params extends unknown[] = any[],
  Result = any,
> = new (...params: Params) => Result;
// > = { new (...params: Params): Result }; // Alt syntax

function withAlteredConstructorArgs<C extends Ctor>(
  ctor: C,
  fn: Fn<ConstructorParameters<C>, ConstructorParameters<C>>,
): Fn<ConstructorParameters<C>, InstanceType<C>> {
  return (...args: ConstructorParameters<C>) => new ctor(...fn(...args));
}

// Use:

// First, an example using a simpler class which DOESN'T take a generic array
// and then use it as the type for its first constructor parameter:
class Person {
  constructor (readonly first: string, readonly last: string) {}
  log () {
    console.log(this.first, this.last);
  }
}

const createPerson = withAlteredConstructorArgs(
  Person,
  (
    first, // correctly inferred as string
    last, // again, string
  ) => [
    first.toLowerCase(),
    last.toUpperCase()
  ] as [string, string], // TS doesn't infer tuples, so an assertion is needed here
);

const p = createPerson('Wolfgang', 'Mozart');
p.log() // "wolfgang" "MOZART"

// Now, your Cycler class:

class Cycler<Elements extends Array<any>> {
  private index = 0;
  constructor(private elements: Elements) {}
  get() {
    const el = this.elements[this.index];
    ++this.index;
    this.index %= this.elements.length
    return el
  }
}

// I inferred that you want this to be generic:
// so that the types of the arguments passed to the function that it RETURNS
// will be used for the resulting Cycler instance
const createCycler = withAlteredConstructorArgs(
  Cycler,
  (elements) => [elements.reverse()] as [unknown[]],
) as <T extends any[]>(elements: T) => Cycler<T>;

const cycler = createCycler(['foo', 'bar', 'baz', 'quux']); // Cycler<string[]>
console.log(cycler.get(), cycler.get()); // "quux" "baz"