2
头图

The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.

The translation of this article is compiled from the chapter Classes

This article does not strictly follow the original translation, but also explains and supplements part of the content.

Classes

TypeScript fully supports the class keyword introduced by ES2015.

Like other JavaScript language features, TypeScript provides type annotations and other syntax, allowing you to express the relationship between classes and other types.

Class Members

This is the most basic class, an empty class:

class Point {}

This class is not very useful, so let's add some members.

Fields

A field declaration creates a public (public) writeable property:

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

Note: The type annotation is optional. If not specified, it will be implicitly set to any .​

Fields can be set to initial values ​​(initializers):

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

Like const , let and var , the initial value of a class attribute will be used to infer its type:

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

--strictPropertyInitialization

strictPropertyInitialization The option controls whether the class field needs to be initialized in the constructor:

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";
  }
}

Note that the fields need to be initialized in the constructor itself. TypeScript does not analyze the methods you call in the constructor to determine the value of initialization, because a derived class may override these methods and fail to initialize members:

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

If you insist on initializing a field by other means, rather than in the constructor (for example, importing an external library to supplement part of the class for you), you can use the definite assignment assertion operator ! :

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

readonly

The field can add a readonly prefix modifier, which will prevent assignment outside of the constructor.

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.

Constructors

The constructor of a class is very similar to a function. You can use type-annotated parameters, default values, overloads, etc.

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
  }
}

But there are also some differences between the signature of the class constructor and the function signature:

  • Constructor functions cannot have type parameters (for type parameters, recall the contents of generics), these belong to the outer class declaration, we will learn about it later.
  • The constructor cannot have return type annotation , because it always returns the class instance type

Super Calls

Just like in JavaScript, if you have a base class, you need super() in the constructor before this. members.

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();
  }
}

Forgetting to call super is a simple error in JavaScript, but TypeScript will alert you when needed.

Methods

The function attributes in the class are called methods. Methods are the same as functions and constructors, using the same type annotations.

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

Except for standard type annotations, TypeScript does not add anything new to methods.

Note that in a method body, it can still access fields and other methods this. An unqualified name in the method body (unqualified name, a name that has no clearly defined scope) always points to the content in the scope of the closure.

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'.
  }
}

Getters / Setter

Classes can also have accessors:

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

TypeScript has some special inference rules for accessors:

  • If get exists but set does not exist, the attribute will be automatically set to readonly
  • If the type of the setter parameter is not specified, it will be inferred as the return type of the getter
  • Getters and setters must have the same member visibility ( Member Visibility ).

Since TypeScript 4.3, accessors can use different types when reading and setting.

class Thing {
  _size = 0;
 
  // 注意这里返回的是 number 类型
  get size(): number {
    return this._size;
  }
 
  // 注意这里允许传入的是 string | number | boolean 类型
  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

A class can declare an index signature, which is the same as the index signature of an object type:

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

Because index signature types also need to capture method types, it is not easy to use these types effectively. Generally speaking, it is better to store index data elsewhere rather than in the class instance itself.

Class Heritage

JavaScript classes can inherit base classes.

implements statement ( implements Clauses)

You can use the implements statement to check whether a class satisfies a specific interface . If a class does not implement it correctly, TypeScript will report an error:

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!");
  }
}

The class can also implement multiple interfaces, such as class C implements A, B {

Cautions

implements statement only checks whether the class is implemented according to the interface type, but it does not change the type of the class or the type of the method. A common mistake is to think that the implements statement will change the type of the class-but in fact it does not:

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
}

In this example, we might think s types are check of name: string parameters influence. In fact, it does not. The implements statement does not affect how the class is checked or type inferred.

Similarly, implementing an interface with optional attributes will not create this attribute:

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 statement ( extends Clauses)

The class can be a base class of extend A derived class has all the properties and methods of the base class, and can also define additional members.

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 attributes (Overriding Methods)

A derived class can override the fields or attributes of a base class. You can use the super syntax to access the methods of the base class.

TypeScript mandates that the derived class is always a subtype of its base class.

For example, this is a legal way to override a method:

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");

The derived class needs to follow the implementation of its base class.

And it is very common and legal to point to a derived class instance through a base class reference:

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

But what if Derived does not follow the contract implementation of 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()}`);
  }
}

Even if we ignore the incorrectly compiled code, this example will run incorrectly:

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

Initialization Order

In some cases, the order in which JavaScript classes are initialized can make you feel weird. Let's look at this example:

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();

What happened?

The order of class initialization is as defined in JavaScript:

  • Base class field initialization
  • Base class constructor running
  • Derived class field initialization
  • Derived class constructor running

This means that the base class constructor can only see its own name , because the derived class field initialization has not yet run.

Inheriting Built-in Types

Note: If you do not intend to inherit the built-in types such as Array , Error , Map etc. or your compilation target is ES6/ES2015 or newer, you can skip this chapter.

In ES2015, when calling super(...) , if the constructor returns an object, it will implicitly replace the value of this So capture super() possible return values and use this replace it is very necessary.

As a result, Error like 061d45c2682362, Array etc., may no longer behave as you expect. This is because the constructors of similar built-in objects such Error and Array new.target adjust the prototype chain. However, in ECMAScript 5, when calling a constructor, there is no way to ensure the value of new.target Other degraded compilers will have the same limitation by default.

For a subclass like the following:

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

You may find:

  1. The method of the object may be undefined , so calling sayHello will cause an error
  2. instanceof invalid, (new MsgError()) instanceof MsgError will return false .

We recommend to manually adjust the prototype after calling super(...)

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

However, any MsgError also had to manually set the prototype. If the runtime does not support Object.setPrototypeOf , you may be able to use __proto__ .

Unfortunately, these solutions will not work properly in IE 10 or earlier versions. One solution is to manually copy the methods in the prototype to the instance (such as MsgError.prototype to this ), but its own prototype chain is still not repaired.

Member Visibility

You can use TypeScript to control whether a method or property is visible to code outside of the class.

public

The default visibility of class members is public , and a public can be obtained anywhere:

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

Because public is the default visibility modifier, you don't need to write it, unless for format or readability reasons.

protected

protected member is only visible to subclasses:

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 members

Derived classes need to follow the implementation of the base class, but you can still choose to expose the base class subtypes with more capabilities. This includes making a protected member become public :

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

It should be noted here that if the disclosure is not intentional, in this derived class, we need to carefully copy the protected modifier.

Cross-hierarchy protected access

Different OOP languages ​​are controversial whether it is possible to legally obtain a protected

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'.
  }
}

In Java, this is legal, while C# and C++ consider this code to be illegal.

TypeScript is on the side of C# and C++. Because Derived2 of x should only be accessed from the Derived2 , and Derived1 not one of them. In addition, if accessing x Derived1 is illegal, accessing through a base class reference should also be illegal.

Read this "Why Can't I Access A Protected Member From A Derived Class?" , which explains more why C# does this.

private

private a bit like protected , but does not allow access to members, even subclasses.

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'.
  }
}

Because private are not visible to derived classes, a derived class cannot increase its visibility:

class Base {
  private x = 0;
}
class Derived extends Base {
// Class 'Derived' incorrectly extends base class 'Base'.
// Property 'x' is private in type 'Base' but not in type 'Derived'.
  x = 1;
}

Cross-instance private access

Different OOP languages ​​are also inconsistent on whether different instances of a class can obtain each other's private Things like Java, C#, C++, Swift and PHP are all allowed, but Ruby is not allowed.

TypeScript allows the acquisition of cross-instance private members:

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

Warning (Caveats)

private and protected are only mandatory for type checking.

This means that when JavaScript is running, members like in or simple attribute lookups can still get members of private or protected

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

private allows access through square bracket syntax during type checking. This makes it easier to access the private field during unit testing, which also makes these fields soft private rather than strictly mandatory.

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

Unlike TypeScript's private , JavaScript's private field ( # ) retains privacy even after compilation, and does not provide a method of obtaining square brackets like the above, which makes them hard private.

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

When compiled into ES2021 or earlier, TypeScript will use WeakMaps instead of # :

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

If you need to prevent malicious attacks and protect the values ​​in the class, you should use strong private mechanisms such as closures, WeakMaps , or private fields. But be aware that this will also affect performance at runtime.

TypeScript series

The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.

WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.

If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.


冴羽
9.3k 声望6.3k 粉丝

17 年开始写前端文章,至今 6 个系列,上百篇文章,全网千万阅读