适合摸鱼看的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);
相关推荐
CodeCraft Studio1 小时前
报表控件stimulsoft教程:使用 JoinType 关系参数创建仪表盘
前端·ui
風吹过2 小时前
A* (AStar) 寻路
typescript·cocos2d
春天姐姐2 小时前
vue知识点总结 依赖注入 动态组件 异步加载
前端·javascript·vue.js
互联网搬砖老肖3 小时前
Web 架构之数据读写分离
前端·架构·web
钢铁男儿3 小时前
C# 方法(值参数和引用参数)
java·前端·c#
阿金要当大魔王~~3 小时前
面试问题(连载。。。。)
前端·javascript·vue.js
yuanyxh4 小时前
commonmark.js 源码阅读(一) - Block Parser
开发语言·前端·javascript
进取星辰4 小时前
22、城堡防御工事——React 19 错误边界与监控
开发语言·前端·javascript
海盐泡泡龟5 小时前
ES6新增Set、Map两种数据结构、WeakMap、WeakSet举例说明详细。(含DeepSeek讲解)
前端·数据结构·es6
t_hj6 小时前
Ajax案例
前端·javascript·ajax