TypeScript之Classes

151 阅读17分钟

TypeScript提供了对class非常丰富的支持。 与其它JavaScript语言功能一样,TypeScript添加了类型注释和其它的语法来让你来表达class其它的类型之间的联系。

类型成员(Class Members)

写一个最基础的class—— 一个空的class

class Point {}

这个class还不是非常有用,让我们添加一些成员

字段(Fields)

字段声明在类上创建公共可写属性:

class Point {
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;

这里对字段类型注释是可选的,但如果未指定,那么将会是any类型。 字段也可以被初始化:

class Point {
  x = 0;
  y = 0;
}
 
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);

就像const,letvar,类的初始化值会决定属性的类型

const pt = new Point();
pt.x = "0";
// Type 'string' is not assignable to type 'number'.

--strictPropertyInitialization

--strictPropertyInitialization可以控制class的字段是否一定需要被构造函数初始化:

class BadGreeter {
  name: string;
  // Property 'name' has no initializer and is not definitely assigned in the constructor.
}
class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

我们注意到字段必须在构造函数内初始化TypeScript不会分析从构造函数调用的方法来检测是否初始化,因为派生类可能会覆盖这些方法并且无法初始化成员。

如果你确定会在以外的构造函数中初始化,那么你也可以使用断言操作符"!":

class OKGreeter {
  // Not initialized, but no error
  name!: string;
}

readonly

字段或许有readonly前缀。这保护字段构造函数外不会被赋值。

class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
 
  err() {
    this.name = "not ok";
        // Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok";
// Cannot assign to 'name' because it is a read-only property.

构造函数(Constructor)

类的构造函数函数非常类似。你可以添加有类型注释的参数,默认值,或者重载

class Point {
  x: number;
  y: number;
 
  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}
class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

类的构造函数函数签名略微有些不同:

  1. 构造函数不能用泛型
  2. 构造函数不能有返回类型声明。

调用super函数(Super Calls)

就像在JavaScript中一样,如果你有一个基类·,那你必须调用super();并且在使用this之前调用:

class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    // Prints a wrong value in ES5; throws exception in ES6
    console.log(this.k);
   // 'super' must be called before accessing 'this' in the constructor of a derived class.
    super();
  }
}

JavaScript中,忘记调用super是非常容易出错的,但是TypeScript会告诉你什么时候需要调用super

方法(Methods)

class中,函数属性被称之为方法方法可以使用和函数构造函数相同的类型注释

class Point {
  x = 10;
  y = 10;
 
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}

除了标准的类型注释TypeScript不需要为方法添加任何其它东西。 请注意在方法内部,必须通过this访问字段。一个不合格的名称:

let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    // This is trying to modify 'x' from line 1, not the class property
    x = "world";
    // Type 'string' is not assignable to type 'number'.
  }
}

在函数体中用没有使用this修饰的变量,将始终引用外层作用域变量

Getters/Setters

class也可以有访问器

class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

TypeScript对于访问器有一些特殊的推断规则:

  1. 如果get存在,但是没有set,那么这个属性会被自动推到为readonly.
  2. 如果setter的参数没有指定类型,那么属性的类型会从getter的返回类型中推导出来。
  3. Getterssetters必须要有相同的访问级别

TypeScript 4.3开始,允许gettingsetting有不同的类型:

class Thing {
  _size = 0;
 
  get size(): number {
    return this._size;
  }
 
  set size(value: string | number | boolean) {
    let num = Number(value);
 
    // Don't allow NaN, Infinity, etc
 
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
 
    this._size = num;
  }
}

索引签名(Index Signatures)

class可以定义索引签名

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
 
  check(s: string) {
    return this[s] as boolean;
  }
}

Class的其它特性(Class Heritage)

和其他的面向对象的语言特点一样,在JavaScript中,class可以在从其它class继承

实现规则(implements Clauses)

你可以使用implements来检查是否满足特定接口。如果class没有正确的实现一个接口的话,那么将会提示一个错误:

interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
// Class 'Ball' incorrectly implements interface 'Pingable'.
// Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
  pong() {
    console.log("pong!");
  }
}

Classes或许也可以实现多个接口,class C implements A, B {

注意事项(Cautions)

重要的是要理解implements字句只是检查类是否可以把视为interface类型。这不会在改变的类型或其方法。一个常见的错误是,认为interface的实现会改变类型,实际上是不会的。

interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) {
    // Parameter 's' implicitly has an 'any' type.
    // Notice no error here
    return s.toLowercse() === "ok";
                 
any
  }
}

在这个例子当中,我们也许希望参数s的类型被自动推导为check的参数namestring类型。但实际上并没有——implements的字句不会改变检查类推断其类型。 类似的,实现一个有可选属性的接口也不会创建任何类型:

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.

extends的细节(extends Clauses)

Class或许会从一个基础classextends(继承)子类拥有基类的所有方法属性,并且也可以定义额外的·成员。

class Animal {
  move() {
    console.log("Moving along!");
  }
}
 
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
 
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);

重写方法(Overriding Methods)

衍生类也可以覆盖基础类的字段或者属性。你可以使用super语法,来访问基类的方法。请注意,因为JavaScript类是一个简单的查找对象,所以没有"super field"的概念。 TypeScript强制派生类始终是其基类子类型。 举个例子,这是重写方法的合法方式:

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}
 
const d = new Derived();
d.greet();
d.greet("reader");

派生类基于基类是非常重要的一种约束。请记住,通过基类引用来引用派生类实例时很常见的:

// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();

如果Derived没有遵守Base呢?:

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  // Make this parameter required
  greet(name: string) {
  // Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
  // Type '(name: string) => void' is not assignable to type '() => void'.
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

如果我们不顾错误编译这段代码,那么可能会发生崩溃性的错误:

const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();

仅类型字段声明(Type-only Field Declarations)

target >= ES2022或者useDefineForClassFieldstrue的时候,class的字段在父类构造函数完成后初始化,覆盖父类设置的任何值。当你想要去为继承字段重新声明更准确的类型时,这会是一个问题。为了处理这些问题,你可以写declare来向TypeScript表明此字段没有运行时影响。

interface Animal {
  dateOfBirth: any;
}
 
interface Dog extends Animal {
  breed: any;
}
 
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
 
class DogHouse extends AnimalHouse {
  // Does not emit JavaScript code,
  // only ensures the types are correct
  declare resident: Dog;
  constructor(dog: Dog) {
    super(dog);
  }
}

初始化顺序(Initialization Order)

在某些情况下,JavaScript类的初始化顺序可能会让人意外:

class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {
  name = "derived";
}
 
// Prints "base", not "derived"
const d = new Derived();

这里发生了什么? 类的初始化顺序,用JavaScript定义就是:

  1. 基类的字段初始化
  2. 积累的构造函数运行
  3. 子类的字段初始化
  4. 子类的构造函数运行

这代表子类构造函数看见的时自己的name值,因为父类的字段至今没有初始化。

继承内置类型(Inheriting Built-in Types)

ES2015中,返回对象的构造函数 隐式地将this的值替换为super(...)的任何调用者。这是非常用用的对于泛型构造函数去捕获任何super(...)潜在的返回值并且替换this·。 因此,ErrorArray,和其它类型或许不再按预期工作。这是由于构造函数对于ErrorArray和像使用ECMAScript 6new.target来调整原型链有关。但是,在ECMAScript5中调用构造函数时,无法确保new.target的值。

一个子类可能看起来像是这样的:

class MsgError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return "hello " + this.message;
  }
}

你或许会发现:

  1. 在对象上,在初始化这些子类的时候,方法或许是undefined,所以调用sayHello可能会发生错误。
  2. instanceof将在子类的实例以及其实例之间中断,所以(new MsgError()) instanceof MsgError会返回false

作为一个建议,你可以在任何super(...)调用后立即手动调整原型

class MsgError extends Error {
  constructor(m: string) {
    super(m);
 
    // Set the prototype explicitly.
    Object.setPrototypeOf(this, MsgError.prototype);
  }
 
  sayHello() {
    return "hello " + this.message;
  }
}

但是,任何MsgError的子类也必须手动设置原型。对于不支持运行时的Object.setPrototypeOf,你可以用__proto__代替。

成员的访问性(Member Visibility)

你可以使用TypeScript来控制方法属性对于外部的可见性。

public

默认的成员可见性是public。一个public的成员可以在任何地方被访问:

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

因为public已经是默认的可见性修饰符,你不再需要显示的添加它。

protected

protected的成员对本类以及子类可见。

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
 
class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.

受保护成员的暴露(Exposure of protected memebers)

子类需要遵守父类的一些规则,但可以选择公开具有更多功能的基类的子类型。例如将受保护的成员公开:

class Base {
  protected m = 10;
}
class Derived extends Base {
  // No modifier, so default is 'public'
  m = 15;
}
const d = new Derived();
console.log(d.m); // OK

请注意,Derived以及能够自由读写m,所以这不会有意义地改变这种情况的“安全性”。这里主要要注意的是,在派生类中,如果这种暴露不是故意的,我们需要小心重复受保护的修饰符`。

跨层次保护访问(Cross-hierarchy protected access)

不同的OOP语言对于通过基类引用访问受保护成员是否合法存在分歧。

class Base {
  protected x: number = 1;
}
class Derived1 extends Base {
  protected x: number = 5;
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }
  f2(other: Base) {
    other.x = 10;
         // Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
  }
}

例如,Java认为这是合法的,但是C#C++选择此代码应该是非法的。

private

privateprotected类似,但是不允许子类访问成员:

class Base {
  private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.
  }
}
Try
Because

因为private的成员在子类中不能被访问,所以子类不能增加它的可见度

class A {
  private x = 10;
 
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}

注意事项(Caveats)

TypeScript类型系统的其他方面,privateprotected只是在编码期间检查。 这就代表在JavaScript的运行时中,类似in或者属性访问依然可以访问private或者protected的成员:

class MySafe {
  private secretKey = 12345;
}
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private还允许在类型检查期间使用括号表示发惊醒访问。这使得私有字段可能更容易访问单元测试等内容,缺点是这些字段是

class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
//  'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);

不像TypeScriptprivateJavaScriptprivate fields(#)在编译后仍然是私有的,并且不提供前面提到的舱口,如括号符号访问 。

class Dog {
  #barkAmount = 0;
  personality = "happy";
 
  constructor() {}
}
"use strict";
class Dog {
    #barkAmount = 0;
    personality = "happy";
    constructor() { }
}
 

当编译到ES2021或者更近的版本,TypeScript将使用WeakMaps代替#

"use strict";
var _Dog_barkAmount;
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();

静态成员(Static Members)

class可能有静态的成员。这些成员不与类的特定实例相关联。它们可以通过访问:

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

静态成员也可以使用publicprotected,和private来修饰:

class MyClass {
  private static x = 0;
}
console.log(MyClass.x);
// Property 'x' is private and only accessible within class 'MyClass'.

静态成员也可以被继承:

class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting();
}

特殊静态名称(Special Static Names)

从函数原型覆盖属性通常是不安全的。因为class本身可以被new调用,但是static的名字不能被new调用。函数的属性namelength,和call不能被再定义:

class S {
  static name = "S!";
// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}

为什么没有静态类(Why No Static Classes)

TypeScript(and JavaScript)没有静态类一样的构造,但是C#这种有。 这些结构之所以存在,是因为这些语言强制所有数据函数都在类中。因为TypeScript中不存在该限制,所以不需要它们。只有一个实例的类通常只标识为JavaScript/TypeScript中的普通对象。

// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

举个例子,我们不需要TypeScript中的"静态类"语法,因为常规对象(甚至顶级函数)也可以完成这项工作:

// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

静态块(Block in Classes)

静态块允许您编写具有自己范围的语句序列,这些句可以访问包含类中的私有字段。这意味着我们可以编写具有静态语句的所有功能的初始化代码,不会泄漏变量,并且可以完全访问我们类的内部。

class Foo {
    static #count = 0;
 
    get count() {
        return Foo.#count;
    }
 
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

泛型类(Generic Classes)

classinterfaces很像,也可以使用泛型。当使用new实例化泛型时,其类型参数的推断方式与函数调用中相同:

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
 
const b = new Box("hello!");
      // const b: Box<string>

静态成员的类型参数(Type Parameters in Static Members)

这个代码不是合法的,这可能不是很明显为什么:

class Box<Type> {
  static defaultValue: Type;
  // Static members cannot reference class type parameters.
}

请记住,类型总是被完全擦除!在运行时的时候,只有一个Box.defaultValue属性槽。这代表设置Box<string>.defaultValue也会改变Box<number>.defaultValue----not good。泛型类的静态成员永远不能引用类的类型参数。

this在运行时(this at Runtime in Classes)

记住TypeScript不会改变JavaScript的运行时行为说非常重要的,并且JavaScript以具有一些特殊的运行时行为而闻名。 JavaScript对此的处理确实不同寻常:

class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};
 
// Prints "obj", not "MyClass"
console.log(obj.getName());

长话短说,默认情况下,函数this的值取决于函数的调用方式。在这个例子中,因为函数是通过obj引用调用的,所以它的this的值是obj而不是类实例。 这很少是你想要发生的!TypeScript提供了一些减轻或防止此类错误的方法。

箭头函数(Arrow Functions)

如果你有一个经常丢失上下文的函数,使用箭头函数而不是方法定义会更有意义:

class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  };
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());

这有一些权衡:

  1. this的值保证在运行时是正确的,即使对于未使用TypeScript检查的代码也是如此。
  2. 这将多使用更多内存,因为每个类实例都会有自己的
  3. 您不能在子类中使用super.getName,因为原型链中没有条目可以从中获取积累方法。

this 参数(this parameters)

在一个方法或者函数定义中,在TypeScript中,this的参数有特殊的意义。这些参数在编译期间被擦除:

// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}
// JavaScript output
function fn(x) {
  /* ... */
}

TypeScript检查是否使用正确的上下文调用带有this参数的函数。我们可以在方法顶一个this参数来静态地强制方法被正确的调用:

class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.

这个方法箭头函数进行了相反的权衡:

  • JavaScript调用者可能仍然不正确地使用类方法而没有意识到
  • 每个类定义只分配一个函数,而不是每个类实例一个
  • 基本方法定义仍然可以通过super调用

this 类型(this Types)

class中,成为this的特殊类型动态引用当前类的类型。让我们看看这有什么用:

class Box {
  contents: string = "";
  set(value: string) {
  // (method) Box.set(value: string): this
    this.contents = value;
    return this;
  }
}

这里,TypeScript推断set的返回类型是this,而不是Box。现在让我们创建Box的子类:

class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}
 
const a = new ClearableBox();
const b = a.set("hello");
     // const b: ClearableBox

你也可以使用this当作类型断言:

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

这和写other:Box是不同的——如果你有一个泛型类,它的sameAs方法现在将只接受同一子类的其它实例:

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}
 
class DerivedBox extends Box {
  otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
  Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.

this类型守卫(this -based type guards)

你可以在接口中的方法的返回位置使用this is Type。当与类型缩小(例如if语句)混合时,目标对象的类型将被缩小到指定类型。

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}
 
class Directory extends FileSystemObject {
  children: FileSystemObject[];
}
 
interface Networked {
  host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
  fso.content;
  // const fso: FileRep
} else if (fso.isDirectory()) {
  fso.children;
  // const fso: Directory
} else if (fso.isNetworked()) {
  fso.host;
  // const fso: Networked & FileSystemObject
}

基于this的类型保护的一个常见用例时允许对特定字段进行延迟验证。举个例子,当hasValue被验证为true时,这种情况会从box内保存的值中删除一个undefined:

class Box<T> {
  value?: T;
 
  hasValue(): this is { value: T } {
    return this.value !== undefined;
  }
}
 
const box = new Box();
box.value = "Gameboy";
 
box.value;
     // (property) Box<unknown>.value?: unknown
 
if (box.hasValue()) {
  box.value;
       //(property) value: unknown
}

参数属性(Parameter Properties)

TypeScript提一个一个特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x);
             // (property) Params.x: number
console.log(a.z);
//Property 'z' is private and only accessible within class 'Params'.

Class表达式(Class Expressions)

类表达式与类声明非常相似。真正的区别只有表达式不需要name,尽管我们可以通过它们最终绑定到的任何标识符来引用它们:

const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
 
const m = new someClass("Hello, world");
     // const m: someClass<string>

抽象类和成员(abstract Classes and Members)

TypeScript中,类,方法,和字段可能是抽象的。 抽象方法或抽象字段时尚未提供实现的方法。这些成员必须存在于抽象类中,不能直接实例化。 抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,就说它时具体的:

abstract class Base {
  abstract getName(): string;
 
  printName() {
    console.log("Hello, " + this.getName());
  }
}
 
const b = new Base();
// Cannot create an instance of an abstract class.

我们不能用new实例化Base,因为它是抽象的。相反,我们需要创建一个派生类并实现抽象成员:

class Derived extends Base {
  getName() {
    return "world";
  }
}
 
const d = new Derived();
d.printName();

请记住如果我们忘记实现了抽象类的成员,那么我们会得到一个误:

class Derived extends Base {
Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
  // forgot to do anything
}

抽象构造函数签名(Abstract Construct Signatures)

有时你想接受一些类构造函数,它产生一个派生自某个抽象类的类的实例:

function greet(ctor: typeof Base) {
  const instance = new ctor();
Cannot create an instance of an abstract class.
  instance.printName();
}

TypeScript告诉你你实例化一个抽象类。毕竟,给定greet的定义,写这段代码是完全合法的,最终会构造一个抽象类

// Bad!
greet(Base);

取而代之的是,你想编写一个接受带有构造签名的东西的函数:

function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
greet(Base);
// Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
  Cannot assign an abstract constructor type to a non-abstract constructor type.

现在TypeScript正确地告诉你可以调用哪些类构造函数。

类之间的联系(Releationships Between Classes)

大多数情况下,TypeScript中的类在结构上进行比较,与其它类型相同。 例如,这两个类可以互相替代使用,因为它们是相同的:

class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
// OK
const p: Point1 = new Point2();

同样,即使没有显示继承,类之间也存在子类型关系:

class Person {
  name: string;
  age: number;
}
 
class Employee {
  name: string;
  age: number;
  salary: number;
}
 
// OK
const p: Person = new Employee();

这听起来很简单,但是有一些案例似乎比其它案例更奇怪。 空类没有成员。在类型系统中,没有成员的类型同时是其他任何类型的父类。所以如果你使用了空类型,任何东西都可以赋值给它:

class Empty {}
 
function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);