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.

Static Members

Classes can have static members. Static members have nothing to do with class instances. They can be accessed through the class itself:

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

Static members can also use the visibility modifiers public protected and private

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

Static members can also be inherited:

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

Special Static Names

The class itself is a function, and overwriting Function is generally considered unsafe, so some fixed static names name , length , call cannot be used to define members of static

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

Why is there no static class? (Why No Static Classes?)

TypeScript (and JavaScript) does not have a structure called static class, but it does exist like C# and Java.

The so-called static class refers to a class that exists inside a certain class as a static member of the class. For example:

// java
public class OuterClass {
  private static String a = "1";
    static class InnerClass {
      private int b = 2;
  }
}

Static classes exist because these languages force all data and functions to be inside a class, but this limitation does not exist in TypeScript, so there is no need for static classes. A class with only one single instance can be replaced by ordinary objects in JavaScript/TypeScript.

For example, we don't need a static class syntax, because a regular object (or top-level function) in TypeScript can achieve the same function:

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

Static Blocks in Classes

The static block allows you to write a series of statements with its own scope, and you can also get private fields in the class. This means that we can write initialization code with peace of mind: normal writing statements, no variable leakage, and full access to properties and methods in the class.

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

Generic Classes

Like interfaces, classes can also be written as generics. When using new instantiate a generic class, its type parameters are inferred in the same way as function calls:

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

Like interfaces, classes can also use generic constraints and default values.

Type Parameters in Static Members

This code is not legal, but the reason may not be so obvious:

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

Remember that the type will be completely erased, at runtime, there is only one attribute slot Box.defaultValue This also means that if Box<string>.defaultValue is possible to set 061d45c1642c82, it will also change Box<number>.defaultValue , which is not good.

Therefore, static members of a generic class should not refer to the type parameters of the class.

this (this at Runtime in Classes)

TypeScript does not change the runtime behavior of JavaScript, and JavaScript sometimes exhibits some strange runtime behavior.

For example, JavaScript processing this very strange:

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

By default, this in the function depends on how the function is called. In this case, because the function by obj is called, so this value is obj not an instance.

This is obviously not what you want. TypeScript provides some ways to mitigate or prevent this kind of error.

Arrow Functions

If you have a function that often loses the this context when it is called, it may be better to use an arrow function.

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

Here are a few things to note:

  • this is correct at runtime, even if TypeScript does not check the code
  • This will use more memory, because every instance of the class will copy this function.
  • You cannot use super.getName in a derived class because there is no entry in the prototype chain to get the base class method.

this parameters (this parameters)

In the definition of a TypeScript method or function, the first parameter and the name this has a special meaning. This parameter will be erased during compilation:

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

TypeScript will check this has the correct context when it is called. Unlike the previous example using arrow functions, we can add a this parameter to the method definition, and the static force method is called correctly:

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

This method also has some points to note, just the opposite of the arrow function:

  • JavaScript callers may still use class methods incorrectly without realizing it
  • One function per class, not one function per class instance
  • The base class method definition can still be called super

this type (this Types)

In the class, there is a special this , which will dynamically reference the type of the current class, let us look at its usage:

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

Here, TypeScript infer set return type is this instead Box . Let's write a Box :

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

// const b: ClearableBox

You can also use this in parameter type annotations:

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

Different from writing other: Box , if you have a derived class, its sameAs method only accepts instances from the same derived class.

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

Type protection based on this (this-based type guards)

this is Type where the methods of the class and interface return. When the collocation type is narrowed (for example, the if sentence), the target object type will be narrowed to a more specific Type .

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
}

A common example of using type protection based on this is to perform lazy validation on a specific field. For example, in this example, when hasValue undefined will be removed from the type:

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 provides a special syntax to convert a constructor parameter into a class attribute with the same name and value. These are called parameter properties. You can create parameter attributes by adding a visibility modifier public private protected or readonly before the constructor parameters, and finally these class attribute fields will also get these modifiers:

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 Expressions

Class expressions are very similar to class declarations. The only difference is that class expressions do not need a name, although we can refer to them by binding identifiers:

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 (abstract Classes and Members)

In TypeScript, classes, methods, and fields can all be abstract.

No implementation is provided for abstract methods or abstract fields. These members must exist in an abstract class, and this abstract class cannot be instantiated directly.

The role of the abstract class is to act as the base class of the subclass, allowing the subclass to implement all abstract members. When a class does not have any abstract members, it is considered concrete.

Let's look at an example:

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

We cannot use the new instance Base because it is an abstract class. We need to write a derived class and implement abstract members.

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

Note that if we forget to implement the abstract member of the base class, we will get an error:

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

Sometimes, you want to accept the incoming class constructor that can inherit some abstract class to produce an instance of the class.

For example, you might write code like this:

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

TypeScript will report an error, telling you that you are trying to instantiate an abstract class. After all, according to greet , this code should be legal:

// Bad!
greet(Base);

But if you write a function that accepts a construction signature:

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.

Now TypeScript will tell you correctly which class constructor can be called. Derived can, because it is specific, but Base cannot.

Relationships Between Classes (Relationships Between Classes)

Most of the time, TypeScript classes are structurally compared like other types.

For example, these two classes can be used to replace each other because their structures are equal:

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

Similarly, a relationship can be established between subtypes of a class, even if there is no obvious inheritance:

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

This sounds a bit simple, but there are some examples to show the strangeness.

The empty class has no members. In a structured type system, a type without members is usually the supertype of any other type. So if you write an empty class (just for example, don’t do that), anything can be used to replace it:

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

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 个系列,上百篇文章,全网千万阅读