The official TypeScript documentation has been updated for a long time, but the Chinese documentation I could find was still in older versions. Therefore, some new and revised chapters are translated and sorted out.

This translation is from the TypeScript Handbook chapter “Classes”.

This paper does not strictly follow the translation of the original text, and some of the content has also been explained and supplemented.

Classes (Classes)

TypeScript fully supports the class keyword introduced in ES2015.

Like other JavaScript language features, TypeScript provides type annotations and other syntax that allow you to express relationships between classes and other types.

Class Members

This is the most basic class, an empty class:

class Point {}
Copy the code

This class is useless, so let’s add some members.

Fields

A field declaration creates a public writeable property:

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

Note: Type annotations are optional and implicitly set to any if not specified.

Fields can be set with initial values:

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

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

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

–strictPropertyInitialization

Whether strictPropertyInitialization options to control the class fields need to be initialized in the constructor:

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

Note that the fields need to be initialized in the constructor itself. TypeScript doesn’t analyze the methods you call in constructors to determine the initialized values, because a derived class might overwrite those 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(); }}Copy the code

If you want to initialize a field by other means than in a constructor (e.g., introducing 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 errorname! :string;
}
Copy the code

readonly

Fields can be added with a readonly prefix modifier, which prevents assignments outside 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.
Copy the code

Constructors

Class constructors are very similar to functions; you can use type-annotated arguments, default values, overloads, and so on.

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

But there are some differences between class constructor signatures and function signatures:

  • Constructors cannot have type parameters (recall from generics about type parameters), which are the outer class declarations, as we will learn later.
  • Constructors cannot have return type annotations because they always return class instance types

Super Calls

Just like in JavaScript, if you have a base class, you need to use any this. Call super() in the constructor before calling the member.

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

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

Methods (Methods)

Function properties in a class are called methods. Methods use the same type annotations as functions and constructors.

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

TypeScript doesn’t add anything new to methods other than standard type annotations.

Note that within a method body, it can still access fields and other methods through this. An unqualified name (which is not explicitly scoped) in the body of a method always refers to something in the closure scope.

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'.}}Copy the code

Getters / Setter

Classes can also have accessors:

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

TypeScript has some special inference rules for accessors:

  • ifgetThere is,setDoes not exist, property is automatically set toreadonly
  • If the type of the setter parameter is not specified, it is inferred to be the return type of the getter
  • Getters and setters must have the same Member Visibility.

Since TypeScript 4.3, accessors can read and set with different types.

class Thing {
  _size = 0;
 
  // Note that the number type is returned
  get size() :number {
    return this._size;
  }
 
  / / note here allow incoming is string | number | Boolean type
  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; }}Copy the code

Index Signatures

Classes can declare index signatures, which are the same as index signatures for object types:

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

Because index signature types also need to capture method types, it is not easy to use them effectively. In general, it is better to store index data somewhere other than in the class instance itself.

Class Heritage

JavaScript classes can inherit from base classes.

implementsStatement (implementsClauses)

You can use the implements statement to check whether a class satisfies a particular interface. If a class does not implement it correctly, TypeScript reports 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!"); }}Copy the code

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

You should learn them skillfully.

The implements statement only checks if the class is implemented according to the interface type, but it does not change the type of the class or method. A common mistake is to think that the implements statement changes the type of the class — when it doesn’t:

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
}
Copy the code

In this case, we might expect the type of S to be affected by the name: string argument to check. It doesn’t. The implements statement doesn’t affect how classes are checked or inferred internally.

Similarly, implementing an interface with an optional attribute does 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'.
Copy the code

extendsStatement (extendsClauses)

Classes can extend a base class. A derived class has all the attributes and methods of the base class and can 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);
Copy the code

Overriding Methods (Ancient Methods)

A derived class can override a field or attribute of a base class. You can access the methods of the base class using super syntax.

TypeScript enforces a derived class to always be a subtype of its base class.

For example, here is a legal way to overwrite 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");
Copy the code

A 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 via a base class reference:

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

But what if Derived doesn’t follow the convention 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()}`); }}Copy the code

This example will run an error even if we ignore the error in compiling the code:

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

Initialization Order

In some cases, the order in which JavaScript classes are initialized may seem strange to you. 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();
Copy the code

So what happened?

The order in which classes are initialized, as defined in JavaScript:

  • Base class field initialization
  • The base class constructor runs
  • Derived class field initialization
  • The derived class constructor runs

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

Inheriting Built-in Types

Note: You can skip this section if you do not intend to inherit built-in types such as Array, Error, Map, etc., or if you are building for ES6/ES2015 or later.

In ES2015, when super(…) is called If the constructor returns an object, the value of this is implicitly replaced. So it is quite necessary to capture the possible return value of super() and replace it with this.

As a result, subclasses such as Error and Array may no longer behave as you expect. This is because constructors for Error, Array, and other built-in objects adjust the prototype chain using ECMAScript 6’s new.target. However, in ECMAScript 5, there is no way to ensure the value of new.target when a constructor is called. Other degraded compilers 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; }}Copy the code

You may find:

  1. The method of the object might beundefined, so callsayHelloCan lead to errors
  2. instanceofFailure,(new MsgError()) instanceof MsgErrorReturns thefalse.

We recommend manual in super(…) Adjust the prototype after the call:

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

However, any subclass of MsgError will also have to set the prototype manually. If the runtime does not support Object.setPrototypeOf, you might use __proto__ instead.

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

Member Visibility

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

public

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

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

Since public is the default visibility modifier, you don’t need to write it except for formatting or readability reasons.

protected

Protected members are visible only 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.
Copy the code

Exposure of protected Members

Derived classes need to follow the implementation of the base class, but you can still choose to expose subtypes of the base class that have more capabilities. This includes making a protected member 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
Copy the code

It is important to note here that we need to be careful to copy the protected modifier in the derived class if the exposure is not intentional.

Cross-hierarchy protected Access

It is debatable whether it is legal for different OOP languages to obtain a protected member through a base class reference.

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'.}}Copy the code

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

TypeScript sides with C# and C++. Because Derived2’s X should only be legal to access from a subclass of Derived2, Derived1 is not one of them. Also, if it is illegal to access X through Derived1, it should be illegal to access x through a base class reference.

See Why Can’t I Access A Protected Member From A Derived Class? “Explains more about why C# does this.

private

Private is 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'.
Copy the code
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
		// Property 'x' is private and only accessible within class 'Base'.}}Copy the code

Since a private member is not visible to a derived class, 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;
}
Copy the code

Cross-instance Private Access

Different OOP languages also differ on whether or not different instances of a class can obtain each other’s private members. Things like Java, C#, C++, Swift and PHP are allowed, Ruby is not.

TypeScript allows cross-instance private member fetching:

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

Caveats

Private and protected are enforced only during type checking.

This means that at JavaScript runtime, you can still get private or protected members for things like in or simple property look-up.

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

Private allows access through square brackets syntax during type checking. This makes it easier to access private fields during unit testing, for example, and it also makes them soft private rather than strictly force-private.

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"]);
Copy the code

Unlike TypeScript’s private, JavaScript private fields (#) remain private even after compilation and don’t provide square bracket access like the one above, making them hard private.

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

When compiled to ES2021 or earlier, TypeScript uses WeakMaps instead of #:

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

If you need to prevent malicious attacks and protect values in your classes, you should use strongly private mechanisms such as closures, WeakMaps, or private fields. Note, however, that this can also affect performance at run time.

The TypeScript series

  1. The basics of TypeScript
  2. Common TypeScript Types (Part 1)
  3. Common TypeScript types (part 2)
  4. TypeScript type narrowing
  5. The function of TypeScript
  6. TypeScript object type
  7. The generic of TypeScript
  8. TypeScript’s Keyof operator
  9. TypeScript’s Typeof operator
  10. TypeScript index access types
  11. TypeScript conditional types
  12. TypeScript mapping types
  13. TypeScript template literal types

Wechat: “MQyqingfeng”, add me Into Hu Yu’s only readership group.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.