理解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"
}
我们声明了一个名为
Foo
的
class
类。在JavaScript中,运行时会创建一个名为
Foo
的
值,它是一个类构造函数。名为
Foo
的值可以像
new Foo()
一样被调用作为构造函数。TypeScript也知道这个值。
此外,
class
声明还引入了一个名为
Foo
的TypeScirpt
类型,它对应于该类的
实例。类型为
Foo
的值应该有一个类型为
string
的
x
属性。
重要的是,
Foo
值的类型不是Foo
类型。它们是不同的。名为
Foo
的类型没有这样的构造签名,但
Foo
值的类型
typeof Foo
具有类似于
new () => Foo
的构造签名。
观察:
const foo: Foo = new Foo();
const fooAlias: = foo;
const fooCtor: typeof Foo = Foo;
const fooCtorAlias: new () => Foo = fooCtor;
const nope: Foo = Foo;
const alsoNope: typeof Foo = new Foo();
foo
和
fooAlias
变量的类型为
Foo
或等效类型,它们保存了
Foo
类的一个实例。
fooCtor
和
fooCtorAlias
变量的类型为
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()
函数需要一个类型为TBase
的Base
参数,该参数受到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());
这段代码告诉你问题的根源:你试图在需要构造函数类型的地方使用了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
哪个更符合惯用语。
代码演示链接
Jumpable()
需要一个构造函数,而不是一个实例;像Jumpable(Test)
而不是Jumpable(new Test())
。这只是一个笔误还是真的存在关于类构造函数和类实例之间的区别的混淆? - jcalzJumpable<Test>
和Jumpable<typeof Test>
(后者可以通过传递的参数进行推断)。 - nullromo