适合摸鱼看的TypeScript学习日记 - 关于类的类型知识

TypeScript 完全支持 ES2015 中引入的 class 关键字。

与其他 JavaScript 语言功能一样,TypeScript 添加了类型注释和其他语法,以允许你表达类和其他类型之间的关系。

类成员

这是最基本的类 - 一个空的:

ts 复制代码
class Point {}

这个类还不是很有用,所以让我们开始添加一些成员。

字段

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

ts 复制代码
class Point {
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;

与其他位置一样,类型注释是可选的,但如果未指定,则为隐式 any

字段也可以有初始化器;这些将在实例化类时自动运行:

ts 复制代码
class Point {
  x = 0;
  y = 0;
}

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

就像 constletvar 一样,类属性的初始化器将用于推断其类型:

ts 复制代码
class Point {
  x = 0;
  y = 0;
}
// ---cut---
const pt = new Point();

// Type 'string' is not assignable to type 'number'.
pt.x = "0";
--strictPropertyInitialization

strictPropertyInitialization 设置控制类字段是否需要在构造函数中初始化。

ts 复制代码
// @errors: 2564 
// Property 'name' has no initializer and is not definitely assigned in the constructor.
class BadGreeter {
  name: string;
}
ts 复制代码
class GoodGreeter {
  name: string;

  constructor() {
    this.name = "hello";
  }
}

请注意,该字段需要在构造函数本身中进行初始化。TypeScript 不会分析你从构造函数调用的方法来检测初始化,因为派生类可能会覆盖这些方法并且无法初始化成员。

如果你打算通过构造函数以外的方式明确地初始化一个字段(例如,可能一个外部库正在为你填充你的类的一部分),你可以使用明确的赋值断言运算符,!

readonly

字段可以以 readonly 修饰符作为前缀。这可以防止对构造函数之外的字段进行赋值。

ts 复制代码
// @errors: 2540 2540
class Greeter {
  readonly name: string = "world";

  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }

  err() {
    //Cannot assign to 'name' because it is a read-only property.
    this.name = "not ok";
  }
}

const g = new Greeter();
//Cannot assign to 'name' because it is a read-only property.
g.name = "also not ok";

构造器

类构造函数与函数非常相似。你可以添加带有类型注释、默认值和重载的参数:

ts 复制代码
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
  }
}

类构造函数签名和函数签名之间只有一些区别:

  • 构造函数不能有类型参数 - 这些属于外部类声明,我们稍后会了解
  • 构造函数不能有返回类型注释 - 类实例类型始终是返回的内容
超类调用

就像在 JavaScript 中一样,如果你有一个基类,在使用任何 this. 成员之前,你需要在构造函数主体中调用 super();

ts 复制代码
// @errors: 17009
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 会在必要时告诉你。

方法

类上的函数属性称为方法。方法可以使用所有与函数和构造函数相同的类型注释:

ts 复制代码
class Point {
  x = 10;
  y = 10;

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

除了标准的类型注解,TypeScript 没有为方法添加任何新的东西。

请注意,在方法体内,仍然必须通过 this. 访问字段和其他方法。方法主体中的非限定名称将始终引用封闭作用域内的某些内容:

ts 复制代码
// @errors: 2322
let x: number = 0;

class C {
  x: string = "hello";

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

获取器/设置器

类也可以有访问器:

ts 复制代码
class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

请注意,没有额外逻辑的由字段支持的 get/set 对在 JavaScript 中很少有用。如果你不需要在 get/set 操作期间添加其他逻辑,则可以公开公共字段。

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

  • 如果 get 存在但没有 set,则属性自动为 readonly
  • 如果不指定 setter 参数的类型,则从 getter 的返回类型推断

从 TypeScript 4.3 开始,可以使用不同类型的访问器来获取和设置。

ts 复制代码
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;
  }
}

索引签名

类可以声明索引签名

ts 复制代码
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);

  check(s: string) {
    return this[s] as boolean;
  }
}

类继承

与其他具有面向对象特性的语言一样,JavaScript 中的类可以从基类继承。

implements 从句

你可以使用 implements 子句来检查一个类是否满足特定的 interface。如果一个类未能正确实现它,则会触发错误:

ts 复制代码
// @errors: 2420
interface Pingable {
  ping(): void;
}

class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}

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

类也可以实现多个接口,例如 class C implements A, B {

注意事项

重要的是要理解 implements 子句只是检查类可以被视为接口类型。它根本不会改变类的类型或其方法。常见的错误来源是假设 implements 子句将更改类类型 - 事实并非如此!

ts 复制代码
// @errors: 7006
interface Checkable {
  check(name: string): boolean;
}

class NameChecker implements Checkable {
  //Parameter 's' implicitly has an 'any' type.
  check(s) {
    // Notice no error here
    return s.toLowerCase() === "ok";
    //         ^?
  }
}

在这个例子中,我们可能预计 s 的类型会受到 checkname: string 参数的影响。它不是 - implements 子句不会更改类主体的检查方式或其类型推断方式。

同样,使用可选属性实现接口不会创建该属性:

ts 复制代码
// @errors: 2339
interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
//Property 'y' does not exist on type 'C'.
c.y = 10;

extends 从句

类可能来自基类。派生类具有其基类的所有属性和方法,还可以定义额外的成员。

ts 复制代码
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);
覆盖方法

派生类也可以覆盖基类字段或属性。你可以使用 super. 语法来访问基类方法。请注意,因为 JavaScript 类是一个简单的查找对象,所以没有 "超级字段" 的概念。

TypeScript 强制派生类始终是其基类的子类型。

例如,这是覆盖方法的合法方式:

ts 复制代码
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");

派生类遵循其基类契约很重要。请记住,通过基类引用来引用派生类实例是很常见的(而且总是合法的!):

ts 复制代码
class Base {
  greet() {
    console.log("Hello, world!");
  }
}
class Derived extends Base {}
const d = new Derived();
// ---cut---
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();


// 如果 `Derived` 不遵守 `Base` 的合约怎么办?

class Derived extends Base {
  // Make this parameter required
  // 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'. Target signature provides too few arguments. Expected 1 or more, but got 0.
  greet(name: string) {
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

#### 仅类型字段声明

当 `target >= ES2022` 或 [`useDefineForClassFields`](https://ts.nodejs.cn/tsconfig#useDefineForClassFields) 为 `true` 时,在父类构造函数完成后初始化类字段,覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能会成为问题。为了处理这些情况,你可以写 `declare` 来向 TypeScript 表明这个字段声明不应该有运行时影响。

// 如果我们在出现错误的情况下编译此代码,则此示例将崩溃:

const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();
仅类型字段声明

target >= ES2022 或 useDefineForClassFields 为 true 时,在父类构造函数完成后初始化类字段,覆盖父类设置的任何值。当你只想为继承的字段重新声明更准确的类型时,这可能会成为问题。为了处理这些情况,你可以写 declare 来向 TypeScript 表明这个字段声明不应该有运行时影响。

ts 复制代码
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);
  }
}
初始化顺序

在某些情况下,JavaScript 类的初始化顺序可能会令人惊讶。让我们考虑这段代码:

ts 复制代码
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 定义的类初始化顺序是:

  • 基类字段被初始化
  • 基类构造函数运行
  • 派生类字段被初始化
  • 派生类构造函数运行

这意味着基类构造函数在其自己的构造函数中看到了自己的 name 值,因为派生类字段初始化尚未运行。

继承内置类型

在 ES2015 中,返回对象的构造函数隐式地将 this 的值替换为 super(...) 的任何调用者。生成的构造函数代码必须捕获 super(...) 的任何潜在返回值并将其替换为 this

因此,ErrorArray 和其他子类可能不再按预期工作。这是因为 ErrorArray 等的构造函数使用 ECMAScript 6 的 new.target 来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target 的值。默认情况下,其他下级编译器通常具有相同的限制。

对于如下子类:

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

你可能会发现:

  • 构造这些子类返回的对象上的方法可能是 undefined,所以调用 sayHello 会导致错误。
  • instanceof 将在子类的实例及其实例之间断开,因此 (new MsgError()) instanceof MsgError 将返回 false

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

ts 复制代码
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

不幸的是,这些解决方法不适用于 Internet Explorer 10 及更早版本。可以手动将原型中的方法复制到实例本身(即 MsgError.prototypethis),但原型链本身无法修复。

成员可见性

你可以使用 TypeScript 来控制某些方法或属性是否对类外部的代码可见。

public

类成员的默认可见性为 publicpublic 成员可以在任何地方访问:

ts 复制代码
class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

因为 public 已经是默认的可见性修饰符,所以你不需要在类成员上编写它,但出于样式/可读性的原因可能会选择这样做。

protected

protected 成员仅对声明它们的类的子类可见。

ts 复制代码
// @errors: 2445
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

//Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
g.getName();
导出 protected 成员

派生类需要遵循其基类契约,但可以选择公开具有更多功能的基类子类型。这包括让 protected 成员成为 public

ts 复制代码
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,因此这并没有有意义地改变这种情况下的 "security"。这里需要注意的主要一点是,在派生类中,如果这种暴露不是故意的,我们需要小心重复 protected 修饰符。

跨层级 protected 访问

TypeScript 不允许访问类层次结构中兄弟类的 protected 成员:

ts 复制代码
// @errors: 2446
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: Derived1) {
    //Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
    other.x = 10;
  }
}

这是因为访问 Derived2 中的 x 应该只在 Derived2 的子类中合法,而 Derived1 不是其中之一。此外,如果通过 Derived1 引用访问 x 是非法的(当然应该如此!),那么通过基类引用访问它永远不会改善这种情况。

private

private 类似于 protected,但不允许从子类访问成员:

ts 复制代码
// @errors: 2341
class Base {
  private x = 0;
}
const b = new Base();
// Can't access from outside the class
// Property 'x' is private and only accessible within class 'Base'.
console.log(b.x);

// @errors: 2341
class Base {
  private x = 0;
}
// ---cut---
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    // Property 'x' is private and only accessible within class 'Base'.
    console.log(this.x);
  }
}

因为 private 成员对派生类不可见,所以派生类不能增加它们的可见性:

ts 复制代码
// @errors: 2415
class Base {
  private x = 0;
}
class Derived extends Base {
  x = 1;
}
跨实例 private 访问

不同的 OOP 语言对于同一类的不同实例是否可以访问彼此的 private 成员存在分歧。虽然 Java、C#、C++、Swift 和 PHP 等语言允许这样做,但 Ruby 不允许。

TypeScript 确实允许跨实例 private 访问:

ts 复制代码
class A {
  private x = 10;

  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}
警告

与 TypeScript 类型系统的其他方面一样,privateprotected 仅在类型检查期间强制执行。

这意味着 in 或简单属性查找之类的 JavaScript 运行时构造仍然可以访问 privateprotected 成员:

ts 复制代码
class MySafe {
  private secretKey = 12345;
}

下面是js代码:

js 复制代码
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private 还允许在类型检查期间使用括号表示法进行访问。这使得 private 声明的字段可能更容易访问,例如单元测试,缺点是这些字段是软私有的并且不严格执行隐私。

ts 复制代码
// @errors: 2341
class MySafe {
  private secretKey = 12345;
}

const s = new MySafe();

// Not allowed during type checking
// Property 'secretKey' is private and only accessible within class 'MySafe'.
console.log(s.secretKey);

// OK
console.log(s["secretKey"]);

与 TypeScripts 的 private 不同,JavaScript 的 私有字段 (#) 在编译后仍然是私有的,并且不提供前面提到的像括号符号访问这样的转义舱口,这使得它们很难私有。

ts 复制代码
class Dog {
  #barkAmount = 0;
  personality = "happy";

  constructor() {}
}

target: esnext

js 复制代码
"use strict";
// @target: esnext
// @showEmit
class Dog {
    #barkAmount;
    constructor() {
        this.#barkAmount = 0;
        this.personality = "happy";
    }
}

target: es2015

js 复制代码
"use strict";
var _Dog_barkAmount;
// @target: es2015
// @showEmit
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();

如果你需要保护类中的值免受恶意行为者的侵害,你应该使用提供硬运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。

静态成员

类可能有 static 个成员。这些成员不与类的特定实例相关联。它们可以通过类构造函数对象本身访问:

ts 复制代码
class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

静态成员也可以使用相同的 publicprotectedprivate 可见性修饰符:

ts 复制代码
// @errors: 2341
class MyClass {
  private static x = 0;
}
// Property 'x' is private and only accessible within class 'MyClass'.
console.log(MyClass.x);

静态成员也被继承:

ts 复制代码
class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting();
}

特殊静态名称

Function 原型覆盖属性通常是不安全/不可能的。因为类本身就是可以用 new 调用的函数,所以不能使用某些 static 名称。namelengthcall 等函数属性无法定义为 static 成员:

ts 复制代码
// @errors: 2699
class S {
// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
  static name = "S!";
}

为什么没有静态类?

TypeScript(和 JavaScript)没有一个名为 static class 的构造,就像 C# 一样。

这些构造之所以存在,是因为这些语言强制所有数据和函数都在一个类中;因为 TypeScript 中不存在该限制,所以不需要它们。只有一个实例的类通常只表示为 JavaScript/TypeScript 中的普通对象。

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

ts 复制代码
// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}

// Preferred (alternative 1)
function doSomething() {}

// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

static 类中的块

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

ts 复制代码
declare function loadLastInstances(): any[]
// ---cut---
class Foo {
    static #count = 0;

    get count() {
        return Foo.#count;
    }

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

泛型类

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

ts 复制代码
class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}

const b = new Box("hello!");

类可以像接口一样使用泛型约束和默认值。

静态成员中的类型参数

此代码不合法​​,原因可能并不明显:

ts 复制代码
// @errors: 2302
class Box<Type> {
   //Static members cannot reference class type parameters.
  static defaultValue: Type;
}

请记住,类型总是被完全擦除!在运行时,只有一个 Box.defaultValue 属性槽。这意味着设置 Box<string>.defaultValue(如果可能的话)也会更改 Box<number>.defaultValue - 不好。泛型类的 static 成员永远不能引用类的类型参数。

类运行时的 this

重要的是要记住,TypeScript 不会改变 JavaScript 的运行时行为,并且 JavaScript 以具有一些特殊的运行时行为而闻名。

JavaScript 对 this 的处理确实不寻常:

ts 复制代码
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 提供了一些减轻或防止此类错误的方法。

箭头函数

如果你有一个经常以丢失其 this 上下文的方式调用的函数,则使用箭头函数属性而不是方法定义是有意义的:

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

这有一些权衡:

  • this 值保证在运行时是正确的,即使对于未使用 TypeScript 检查的代码也是如此
  • 这将使用更多内存,因为每个类实例都会有自己的每个以这种方式定义的函数的副本
  • 你不能在派生类中使用 super.getName,因为原型链中没有条目可以从中获取基类方法

this 参数

在方法或函数定义中,名为 this 的初始参数在 TypeScript 中具有特殊含义。这些参数在编译期间被删除:

ts 复制代码
type SomeType = any;
// ---cut---
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}
js 复制代码
// JavaScript output
function fn(x) {
  /* ... */
}

TypeScript 检查是否使用正确的上下文调用带有 this 参数的函数。我们可以不使用箭头函数,而是在方法定义中添加一个 this 参数,以静态强制方法被正确调用:

ts 复制代码
// @errors: 2684
class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();

// Error, would crash
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
const g = c.getName;

console.log(g());

此方法与箭头函数方法进行了相反的权衡:

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

参数属性

TypeScript 提供了特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些称为参数属性,是通过在构造函数参数前加上可见性修饰符 publicprivateprotectedreadonly 之一来创建的。结果字段获取这些修饰符:

ts 复制代码
// @errors: 2341
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 'z' is private and only accessible within class 'Params'.
console.log(a.z);

类表达式

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

ts 复制代码
const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};

const m = new someClass("Hello, world");

构造函数签名

JavaScript 类是使用 new 运算符实例化的。给定类本身的类型,InstanceType 工具类型会对此操作进行建模。

ts 复制代码
class Point {
  createdAt: number;
  x: number;
  y: number
  constructor(x: number, y: number) {
    this.createdAt = Date.now()
    this.x = x;
    this.y = y;
  }
}
type PointInstance = InstanceType<typeof Point>

function moveRight(point: PointInstance) {
  point.x += 5;
}

const point = new Point(3, 4);
moveRight(point);
point.x; // => 8

abstract 类和成员

TypeScript 中的类、方法和字段可能是抽象的。

抽象方法或抽象字段是尚未提供实现的方法。这些成员必须存在于抽象类中,不能直接实例化。

抽象类的作用是作为实现所有抽象成员的子类的基类。当一个类没有任何抽象成员时,就说它是具体的。

让我们看一个例子:

ts 复制代码
// @errors: 2511
abstract class Base {
  abstract getName(): string;

  printName() {
    console.log("Hello, " + this.getName());
  }
}

const b = new Base();

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

ts 复制代码
abstract class Base {
  abstract getName(): string;
  printName() {}
}
// ---cut---
class Derived extends Base {
  getName() {
    return "world";
  }
}

const d = new Derived();
d.printName();

请注意,如果我们忘记实现基类的抽象成员,我们会得到一个错误:

ts 复制代码
// @errors: 2515
abstract class Base {
  abstract getName(): string;
  printName() {}
}
// ---cut---
// Non-abstract class 'Derived' does not implement inherited abstract member getName from class 'Base'.
class Derived extends Base {
  // forgot to do anything
}

抽象构造签名

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

例如,你可能想编写以下代码, TypeScript 正确地告诉你你正在尝试实例化一个抽象类。毕竟,给定 greet 的定义,编写这段代码是完全合法的,它最终会构造一个抽象类:

ts 复制代码
function greet(ctor: typeof Base) {
   //Cannot create an instance of an abstract class.
  const instance = new ctor();
  instance.printName();
}

// Bad!
greet(Base);

相反,你想编写一个接受带有构造签名的东西的函数:

ts 复制代码
// @errors: 2345
abstract class Base {
  abstract getName(): string;
  printName() {}
}
class Derived extends Base {
  getName() {
    return "";
  }
}
// ---cut---
function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
//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.
greet(Base);

现在 TypeScript 可以正确告诉你可以调用哪些类构造函数 - Derived 可以,因为它是具体的,但 Base 不能。

类之间的关系

在大多数情况下,TypeScript 中的类在结构上进行比较,与其他类型相同。

例如,这两个类可以互相代替使用,因为它们是相同的:

ts 复制代码
class Point1 {
  x = 0;
  y = 0;
}

class Point2 {
  x = 0;
  y = 0;
}

// OK
const p: Point1 = new Point2();

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

ts 复制代码
// @strict: false
class Person {
  name: string;
  age: number;
}

class Employee {
  name: string;
  age: number;
  salary: number;
}

// OK
const p: Person = new Employee();

这听起来很简单,但有一些案例似乎比其他案例更奇怪。

空类没有成员。在结构类型系统中,没有成员的类型通常是其他任何东西的超类型。所以如果你写一个空类(不要!),任何东西都可以用来代替它:

ts 复制代码
class Empty {}

function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}

// All OK!
fn(window);
fn({});
fn(fn);
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax