TypeScript泛型类约束。

3

我有一个例子,描述了要传递给Jumpable类的结构。当我传递Test类时,出现了错误。

type GConstructor<T> = new (...args: any[]) => T;

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      this.setPos(0, 20);
    }
  };
}

class Test {
  constructor() {
    console.log();
  }

  setPos(x: number, y: number) {
    console.log();
  }
}

Jumpable<Test>(new Test());

类型“Test”不满足约束“Positionable”。类型“Test”没有为签名“new (...args: any[]): { setPos: (x: number, y: number) => void; }”提供匹配项。.ts(2344)


通过这个链接,我认为我已经证明了这与构造函数的签名/类型有关,而不是方法。 - Daniel Kaplan
1
Jumpable()需要一个构造函数,而不是一个实例;像Jumpable(Test)而不是Jumpable(new Test())。这只是一个笔误还是真的存在关于类构造函数和类实例之间的区别的混淆? - jcalz
@jcalz 我只是测试时遇到了相同的错误。 - Nick
不,它运行良好。那么这不仅仅是一个打字错误吗? - jcalz
区别在于 Jumpable<Test>Jumpable<typeof Test>(后者可以通过传递的参数进行推断)。 - nullromo
显示剩余5条评论
1个回答

4

理解JavaScript 和TypeScript 类型之间的区别非常重要,它们存在的时间不同。值存在于运行时,而类型仅在编译时存在。

类型有点像可能值的集合。在以下代码中:

let x = "hello";

变量x将在运行时保存值"hello",而TypeScript编译器会推断它的类型为string。因此,"hello"是构成string类型的可能值之一,这没有问题。我们可以说"变量x的类型是string"。

如果你有一个像x这样的变量,你可以使用TypeScript的typeof类型查询运算符在类型上下文中查询其类型(不要与JavaScript的typeof操作符混淆):

type TypeofX = typeof x;
// type TypeofX = string

值和类型不同, TypeScript 中的值与类型之间通常有明显且明确的区别,或者差异足够小可以忽略。例如,“单例”或“单位”类型只对应一个值,通常会使用与该值相同的名称,比如字符串字面量类型"hello"对应的值为"hello",这时你可以将其视为同一概念。

但有时候一个值和一个类型可能会共享一个名称,但是它们并不能直接对应。主要情况出现在 class 声明中。

接下来是示例:

class Foo {
    x: string = "hello"
}

我们声明了一个名为Fooclass类。在JavaScript中,运行时会创建一个名为Foo,它是一个类构造函数。名为Foo的值可以像new Foo()一样被调用作为构造函数。TypeScript也知道这个值。
此外,class声明还引入了一个名为Foo的TypeScirpt 类型,它对应于该类的实例。类型为Foo的值应该有一个类型为stringx属性。
重要的是,Foo值的类型不是Foo类型。它们是不同的。名为Foo的类型没有这样的构造签名,但Foo值的类型typeof Foo具有类似于new () => Foo的构造签名。
观察:
const foo: Foo = new Foo();
const fooAlias: { x: string } = foo;

const fooCtor: typeof Foo = Foo;
const fooCtorAlias: new () => Foo = fooCtor;

const nope: Foo = Foo; // error! 
// Property 'x' is missing in type 'typeof Foo' but required in type 'Foo'
const alsoNope: typeof Foo = new Foo(); // error!
// Property 'prototype' is missing in type 'Foo' but required in type 'typeof Foo'
foofooAlias 变量的类型为 Foo 或等效类型,它们保存了 Foo 类的一个实例。 fooCtorfooCtorAlias 变量的类型为 typeof Foo 或等效类型,它们保存了 Foo 类的构造函数。如果你试图将其中一个赋给另一个,则会出现错误。
所以,让我们看一下您的代码:
type GConstructor<T> = new (...args: any[]) => T;

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      this.setPos(0, 20);
    }
  };
}

这里,Jumpable()函数需要一个类型为TBaseBase参数,该参数受到Positionable的限制,Positionable是一种具有构造函数签名的类型,用于构造类型为{ setPos(x: number, y: number) => void }的实例。一个Positionable必须是一个类构造函数。所有这些都是有道理的,因为Jumpable()返回一个新的类构造函数,它是传入的Base的子类。看起来像是一个混合类工厂。mix-in类

Jumpable()希望其输入是一个类构造函数。

然而,当你调用它时:

Jumpable<Test>(new Test());

您已经将类型为 Test 的值传递给它。 Jumpable() 不希望接受类型为 Test 的值; 那是一个类的实例,而不是一个类的构造函数。您会收到以下错误提示:
Jumpable<Test>(new Test()); // error!
// Type 'Test' does not satisfy the constraint 'Positionable'.
// Type 'Test' provides no match for the signature 
// 'new (...args: any[]): { setPos: (x: number, y: number) => void; }'

这段代码告诉你问题的根源:你试图在需要构造函数类型的地方使用了Test实例类型。 Test不是构造函数类型,它没有构造签名,是错误的做法。如果你编译并运行该代码,你也会得到一个运行时错误,如下:

//  Base is not a constructor 

正确的做法是给Jumpable()提供它需要的类构造函数。假设你想创建Test的一个子类,那么应该直接传递Test的构造函数:
Jumpable<typeof Test>(Test) // okay

typeof Test类型可以赋值给new () => Test,后者可以赋值给new (...args: any[]) => Test,后者又可赋值给new (...args: any[]) => { setPos: (x: number, y: number) => void; },因此编译器很高兴。更好的是,这段代码在运行时也能正常工作。

请注意,如果编译器可以推断出类型参数,则无需指定类型参数,因此这与以下代码效果相同。

Jumpable(Test); // also okay

哪个更符合惯用语。


代码演示链接


jcalz 给出了另一个清晰而详尽的答案。我以前从未完全理解声明类会在运行时创建值和 TypeScript 类型,就像这样。"Foo 值的类型不是 Foo 类型" 是很重要的。现在我明白了类构造函数和类类型之间的区别。 - nullromo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接