【问题标题】:Infer class constructor arguments from instance of class in TypeScript从 TypeScript 中的类实例推断类构造函数参数
【发布时间】:2021-02-26 16:06:29
【问题描述】:

我有一个单例类,它是 20x20 数组的扩展。 (我正在搜索单词。)我发现了这种用于导出单例类的实例的令人愉快的语法,它同时维护了实例的名称和类的名称:

const Grid = new class Grid extends Array<string[]> {
  constructor(private readonly size = 20) {
    super(size);
    // ...
  }

  // ...
};

export default Grid;

Huzzah,效果很好。现在来测试。为了使事情易于管理,我想创建一个尺寸更小的新网格(可能是 8x8)。在 JavaScript 中,检索构造函数很容易。

import Grid from './grid';

const GridConstructor = Grid.constructor;

it('does a thing', () => {
  const testGrid = new GridConstructor(8);
});

因为我知道参数期望什么,是的,我可以这样做并将GridConstructor声明为

const GridConstructor: new(size: number) => typeof Grid = Grid.constructor;

当然,这是合理的解决方案。不过,我不是一个理性的人,并且必须使用 TypeScript 打字引擎输入所有的东西。 (好吧,所以我真的很喜欢学习。谁知道这什么时候有用?) TypeScript 认为 Grid.constructorFunction 类型。据我所知,它没有任何关于该函数形状的信息。

可以推断出那个形状吗?

我想到的最好的是:

type Constructor<T extends { constructor: new(...args: any[]) => any; }> = 
  new(...args: ConstructorParameters<T['constructor']>) => InstanceType<T['constructor']>;

const GridConstructor: Constructor<typeof Grid> = Grid.constructor;

这会导致错误:

Type 'Grid' does not satisfy the constraint '{ constructor: new (...args: any[]) =&gt; any; }'.

TypeScript Playground

【问题讨论】:

    标签: typescript


    【解决方案1】:

    这是目前 TypeScript 中的一个限制。类实例没有强类型的constructor 属性;如您所述,编译器仅将其类型视为Function。有一个长期悬而未决的问题, microsoft/TypeScript#3841,要求对此进行更改。但是“明显的”修复,其中编译器使名为Foo 的类的实例具有constructor 类型为typeof Foo 的属性,会破坏事情。该语言的架构师在this comment 中对此进行了解释:

    我们在这里面临的一般问题是,许多有效的 JavaScript 代码派生的构造函数不是其基本构造函数的正确子类型。换句话说,派生构造函数“违反”了类静态方面的替换原则。

    例如:

    class Foo {
      x: string;
      constructor(x: string) {
        this.x = x.toUpperCase();
      }
    }
    

    假设每个值foo 其中foo instanceof Foo 都有一个constructor 类型为typeof Foo 的属性。然后编译器将允许您编写以下函数而无需类型断言:

    function cloneFoo(foo: Foo): Foo {
      const ctor = foo.constructor as typeof Foo; // need assertion here in current TS
      return new ctor(foo.x); 
    }
    

    那么只要你只是处理Foo本身,就不会有问题:

    cloneFoo(new Foo("x")); // okay
    

    但是,JavaScript 有 class 层次结构,其中子类构造函数需要一组与其基类不同的参数,TypeScript 也支持这一点:

    class Bar extends Foo {
      y: string;
      constructor(x: string, y: string) {
        super(x);
        this.y = y.toUpperCase();
      }
    }
    

    这里,Bar 的构造函数需要第二个参数,而Foo 不需要。但是Bar 的每个实例也是Foo 的一个实例,根据subtitutability principle 和JS 运行时(例如,new Bar("x", "y") instanceof Foo 的计算结果为true)。

    但这直接导致了一个问题。如果每个Bar 实例也是Foo 实例,那么每个Bar 实例的constructor 属性必须是有效的typeof Foo但事实并非如此:

    cloneFoo(new Bar("x", "y")); // error at runtime! y is undefined
    

    我们获得了一个强类型的构造函数,但作为交换得到了运行时错误。为了防止这些错误,您要么需要放弃可分配给超类实例的子类实例,从而使 extends 用词不当......要么您必须限制子类构造函数,以便您可以需要与超类,这将是一个“巨大的突破性变化”,因为大量现有的 JavaScript class 层次结构违反了这条规则。

    因此,虽然 constructor 的当前类型只是 Function 有点没用,但它至少不会破坏类层次结构或类型系统,这很好。可能有比Function 更好的东西,但没有任何东西可以安全地用new 调用。

    因此,这个问题多年来一直没有受到影响。


    请注意,在您的情况下,子类显然是不可能的,因为您有一个 private 属性,这使得事情难以扩展。因此,也许可以打开一个新问题(或对现有问题发表评论),要求在具有private 属性的任何类上对constructor 进行强类型化。从某种意义上说,这将在 TypeScript 中创建真正的 final 类......但这已经在 microsoft/TypeScript#8306 中提出并被拒绝。

    不管怎样,就目前而言,就是这样。


    如果知道使用强类型constructor 是安全的,那么类型断言是一个合理的解决方案;但这无异于你已经在做的事情:

    const GridConstructor = Grid.constructor as new (size?: number) => typeof Grid;
    

    这里的另一种解决方法是手动声明强类型的constructor 属性:

    const Grid = new class _Grid extends Array<string[]> {
      ["constructor"]: typeof _Grid
      constructor(private readonly size = 20) {
        super(size);
      }
    }
    

    constructor 周围的引号和方括号是避免错误所必需的......并且从 Grid 重命名为 _Grid 不是必需的,但有助于避免混淆我们正在谈论的内容)。

    当然,这并不比类型断言好;你是在强迫自己自己写出构造函数类型。


    所以我想说这里的答案只是;编译器故意不允许您从实例的类型推断构造函数参数的类型,并且不清楚何时或是否会改变。

    Playground link to code

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-26
      • 1970-01-01
      • 2021-05-30
      • 1970-01-01
      • 2019-02-07
      • 2020-10-17
      • 1970-01-01
      相关资源
      最近更新 更多