TypeScript official manual translation plan [12]: Category

Posted by PJNEW on Thu, 09 Dec 2021 12:57:36 +0100

  • Note: at present, there is no Chinese translation of the latest official documents of TypeScript on the Internet, so there is such a translation plan. Because I am also a beginner of TypeScript, I can't guarantee the 100% accuracy of translation. If there are errors, please point them out in the comment area;
  • Translation content: the tentative translation content is TypeScript Handbook , other parts of the translated documents will be supplemented when available;
  • Project address: TypeScript-Doc-Zh , if it helps you, you can click a star~

Official document address of this chapter: Classes

Background Guide: Class (MDN)

class

TypeScript provides comprehensive support for the class keyword introduced in ES2015.

Like other JavaScript language features, TypeScript also provides type annotations and other syntax for classes to help developers express the relationship between classes and other types.

Class member

This is the most basic class - it is empty:

class Point {}

This class is of little use at the moment, so let's add some members to it.

field

The declaration field is equivalent to adding a public and writable attribute to the class:

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

Like other features, the type annotation here is optional, but if no type is specified, the any type is implicitly adopted.

Fields can also be initialized. The initialization process will be carried out automatically when the class is instantiated:

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

Just like const, let and var, the initialization statement of class attributes will also be used for type inference:

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

Configuration item strictPropertyInitialization Used to control whether the fields of the class need 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 fields need to be initialized inside the constructor itself. TypeScript does not analyze the methods invoked in the constructor to detect initialization statements, because the derived classes may override these methods, causing the initialization members to fail.

If you insist on initializing a field using methods other than constructors (such as filling the content of a class with an external library), you can use the deterministic assignment assertion operator!:

class OKGreeter {
  // No initialization, but no error will be reported
  name!: string;
}

readonly

Fields can be prefixed with the readonly modifier to prevent assignment of fields 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.

constructor

The constructor of a class is very similar to a function. You can add type annotations to its parameters. You can use the default value of parameters or function overloading:

class Point {
    x: number;
    y: number;
    // Normal signature using default value of parameter
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
}
class Point {
  // Use overload
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

There is only one difference between class constructor signature and function signature:

  • Constructors cannot use type parameters -- type parameters are part of class declarations, which we'll learn later
  • A constructor cannot add type annotations to a return value -- the type it returns is always the type of the class instance
super call

Like JavaScript, if you have a base class and a derived class, use this in the derived class Before you access a class member, you must first call super() in the constructor;

class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    // The wrong value is printed under ES5 and an error is reported under ES6
    console.log(this.k);
                  ^
// 'super' must be called before accessing 'this' in the constructor of a derived class.
    super();
  }
}

In JavaScript, forgetting to call super is a common error, but TypeScript will remind you when necessary.

method

The property of a class may be a function. At this time, we call it a method. Methods, like functions and constructors, can also use various types of annotations:

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

TypeScript adds nothing new to methods other than standard type annotations.

Note that in the method body, you must pass this To access the fields and other methods of the class. Using a non-conforming name in the method body will be regarded as accessing variables in the adjacent scope:

let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    // The following sentence is trying to modify the x in the first line, not the attribute of the class
    x = "world";
    ^  
// Type 'string' is not assignable to type 'number'.
  }
}

Getters/Setters

Class can also have accessors:

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

Note: in JavaScript, a get/set pair without additional logic has no effect. If you do not need to add additional logic when performing the get/set operation, you only need to expose the field as a public field.

TypeScript has some special inference rules for accessors:

  • If get exists but set does not, the property will automatically become read-only
  • If the type of the setter parameter is not specified, the parameter type will be inferred based on the type of the returned value of the getter
  • getter and setter must have the same Member visibility.

from TypeScript 4.3 Initially, getter s and setter s of accessors can use different types.

class Thing {
  _size = 0;
 
  get size(): number {
    return this._size;
  }
 
  set size(value: string | number | boolean) {
    let num = Number(value);
 
    // NaN, Infinity, etc. are not allowed
 
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
 
    this._size = num;
  }
}

Index signature

Class can declare an index signature, which works and Index signatures for other object types Same:

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

Because index signature types also need to capture the types of methods, it is not easy to use these types effectively. In general, it is better to store the index data in another location than the class instance itself.

Class inheritance

Like other object-oriented languages, classes in JavaScript can inherit from base classes.

implements clause

You can use an implements clause to check whether a class conforms to a specific interface. If the class does not implement this interface correctly, an error will be thrown:

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

Class can implement multiple interfaces, such as class C implements A,B {.

matters needing attention

One important point to understand is that the implements clause is only used to check whether a class can be regarded as an interface type, and it will not change the type of the class or its methods at all. The common mistake is to think that the implements clause will change the type of the class - in fact, it will not!

interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) {
        ^
//Parameter 's' implicitly has an 'any' type.
    // Note that no errors are thrown here
    return s.toLowercse() === "ok";
                 ^
              // any
  }
}

In this example, we may think that the type of s will be affected by the name: string parameter of check in the interface, but actually not -- the implements clause will not have any impact on the inspection of the class content body and type inference.

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

Extensions clause

Class can inherit from a base class. Derived classes have all the properties and methods of the base class, and they 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);
override method

Derived classes can also override the fields or properties of the base class. You can use the super. Syntax to access the methods of the base class. Note that since the JavaScript class is just a simple lookup object, there is no concept of "parent class field".

TypeScript forces that a derived class is always a subclass of the base class.

For example, the following is an example of a legal rewriting 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");

It is important that derived classes follow the constraints of the base class. It is common (and always legal!) to refer to a derived class through a base class reference One way to do this:

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

What happens if the Derived class Derived does not follow the constraints of the Base class?

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  // Make this parameter a required parameter
  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()}`);
  }
}

If you ignore the error and compile the code, the following code will report an error after execution:

const b: Base = new Derived();
// Because name is undefined, an error is reported
b.greet();
Initialization sequence

The initialization order of JavaScript classes may surprise you in some cases. Let's look at the following code:

class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {
  name = "derived";
}
 
// Print base instead of derived
const d = new Derived();

What happened here?

According to the definition of JavaScript, the order of class initialization is:

  • Initializes the fields of the base class
  • Constructor that executes the base class
  • Initializes the fields of the derived class
  • Constructor that executes the derived class

This means that because the derived class fields have not been initialized when the base class constructor executes, the base class constructor can only see its own name value.

Inherit built-in types

Note: if you do not intend to inherit built-in types such as Array, Error and Map, or your compilation target is explicitly set to ES6/ES2015 or later, you can skip this part.

In ES2015, the constructor that returns the instance object implicitly replaces the value of this with super(...) Any caller of. It is necessary for the generated constructor code to capture super(...) And replace it with this.

Therefore, subclasses of Error, Array, etc. may not work as expected. This is because constructors such as Error and Array use new in ES6 Target to adjust the prototype chain, however, when calling constructor function in ES5, there is no similar way to ensure new.. The value of target. By default, other underlying compilers usually have the same limitations.

For a subclass like the following:

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

You may find:

  • The method of the instance object returned after calling the subclass may be undefined, so calling sayHello will throw an error
  • The instanceof between subclass instances and subclasses may be destroyed, so (new MsgError()) instanceof MsgError will return false.

The recommended practice is in any super(...) Manually adjust the prototype chain after calling:

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

However, any subclass of MsgError also needs to be prototyped manually. Not supported for Object.setPrototypeOf When running, you can use __proto__.

Unfortunately, These workarounds cannot be used on IE10 or older versions . aspx). You can manually copy the method on the prototype to the instance (for example, copy the method of MsgError.prototype to this), but the prototype chain itself cannot be repaired.

Member visibility

You can use TypeScript to control whether specific methods or properties are visible outside the class.

public

The default visibility of class members is public. Public members can access anywhere:

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

Since the visibility of members is public by default, you don't need to explicitly declare them in front of class members, but you can do so for the sake of code specification or readability.

protected

protected members are visible only in subclasses of a class.

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
 
class SpecialGreeter extends Greeter {
  public howdy() {
    // Protected members can be accessed 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.
Public protected members

A derived class needs to follow the constraints of its base class, but you can choose to expose subclasses of a base class with more functions. This includes making protected members public:

class Base {
    protected m = 10;
}
class Derived extends Base {
    // There is no modifier, so the default visibility is public
    m = 15;
}
const d = new Dervied();
console.log(d.m);  // OK

Note that Dervied can read and write member m freely, so writing this does not change the "security" of this situation. The important point to note here is that in a derived class, if we do not intend to expose its members, we need to add the protected modifier.

Cross level access to protected members

There are disputes between different OOP languages about whether it is legal to access protected members 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'.                  
    }
}

For example, Java thinks the above code is legal, but c# and C + + think the above code is illegal.

TypeScript also thinks this is illegal, because it is only legal to access x of Derived2 in a subclass of Derived2, but Derived1 is not a subclass of Derived2. Moreover, it is illegal to access x through Derived1 reference (which should be illegal indeed!), It should also be illegal to access it through a base class reference.

For more information on why C# thinks this code is illegal, read this article: Why can't I access a protected member in a derived class?

private

Private is the same as protected, but private members that declare private cannot be accessed even in subclasses:

class Base {
    private x = 0;
}
const b = new Base();
// Cannot access outside class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
  showX() {
    // Cannot access in subclass
    console.log(this.x);
                      ^    
// Property 'x' is private and only accessible within class 'Base'.
  }
}

The derived class cannot improve its visibility because private members are not visible to the derived class:

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

Different OOP languages dispute whether it is legal for different instances of the same class to access each other's private members. Java, c#, C + +, Swift, and PHP allow this, but Ruby thinks it's illegal.

TypeScript allows access to private members across instances:

class A {
    private x = 10;
    public sameAs(other: A) {
        // No error will be reported
        return other.x === this.x;
    }
}
matters needing attention

Like everything else in the TypeScript type system, private and protected Effective only during type check.

This means that some operations of JavaScript runtime, such as in or simple attribute lookup, can still access private members or protected members:

class MySafe {
    private serectKey = 123345;
}
// 12345 is printed in the JavaScript file
const s = new MySafe();
console.log(s.secretKey);

Even during type checking, we can access private members through square bracket syntax. Therefore, it is easier to access private fields during operations such as unit testing, but the disadvantage is that these fields are "weakly private" and cannot guarantee privacy in the strict sense.

class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// This access to private members is not allowed during type checking
console.log(s.secretKey);
                ^
// Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// However, it can be accessed through square bracket syntax
console.log(s["secretKey"]);

Unlike the private members declared by TypeScript with # private, JavaScript is declared with Private field It is still private after compilation, and there is no square bracket syntax like the above to access private members, so the private members of JavaScript are "strongly private".

class Dog {
    #barkAmount = 0;
    personality = 'happy';
    
    constructor() {}
}

Take the following TypeScript code as an example:

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

After compiling it into ES2021 or earlier, TypeScript uses WeakMap instead #.

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

If you need to protect the values in the class from malicious modification, you should use a mechanism that provides run-time privacy protection, such as closures, weakmaps or private fields. Note that these privacy checks added at run time may affect performance.

Static member

Background Guide: Static member (MDN)

Classes can have static members. These members are independent of the specific instance of the class. We can access them through the class constructor object itself:

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

Static members can also use visibility modifiers such as 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 member name

Rewriting the properties of a function prototype is usually unsafe / impossible. Because the class itself is also a function that can be called through new, some specific static member names cannot be used. Function properties such as name, length, and call cannot be used as the names of static members:

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?

TypeScript (and JavaScript) does not provide a static class structure like C# and Java.

C# and Java need static classes because these languages require that all data and functions must be placed in one class. Because this restriction does not exist in TypeScirpt, static classes are not required. Classes with only a single instance are usually represented by a common object in JavaScript/TypeScirpt.

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

// Unnecessary static classes
class MyStaticClass {
    static doSomething() {}
}
// Preferred (scheme I)
function doSomething() {}

// Preferred (scheme II)
const MyHelperObject = {
  dosomething() {},
};

Static block in class

Static blocks allow you to write a series of declaration statements that have their own scope and can access private fields in the containing class. This means that we can write initialization code, which contains declaration statements, will not have the problem of variable leakage, and can fully access the interior of the class.

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

Generic class

Classes, like interfaces, can also use generics. When a generic class is instantiated with new, its type parameters are inferred as in the function call:

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

Classes can use generic constraints and defaults like interfaces.

Type parameters in static members

The following code is illegal, but the reason may not be so obvious:

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

Remember, types are always completely erased after compilation! At run time, there is only one box The DefaultValue property the slot. This means setting box < string > The DefaultValue (if it can be set) will also change box < number > DefaultValue -- this is not possible. Static members of a generic class can never reference type parameters of a class.

Class's runtime this

One important thing to remember is that TypeScript does not change the runtime behavior of JavaScript. As we all know, JavaScript has some special runtime behavior.

JavaScript's handling of this is really unusual:

class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};
 
// Print "obj" instead of "MyClass"
console.log(obj.getName());

To make a long story short, by default, the value of this in a function depends on how the function is called. In this example, since we call the function through the obj reference, the value of this is obj, not the class instance.

This is usually not the result we expect! TypeScript provides some methods to reduce or prevent this error.

Arrow function

If your function often loses this context when called, it is better to use the arrow function attribute instead of the method definition:

class MyClass {
    name = 'MyClass';
    getName = () => {
        return this.name;
    };
}
const c = new MyClass();
const g = c.getName;
// Print MyClass 
console.log(g());

This approach has some trade-offs:

  • The value of this can be guaranteed to be correct at runtime, even for code that does not use TypeScript for checking
  • This will take up more memory, because functions defined in this way will result in a copy of the function for each class instance
  • You cannot use super. In a derived class Getname, because there is no entry on the prototype chain to obtain the method of the base class

this parameter

In the method or function definition of TypeScript, if the name of the first parameter is this, it has special meaning. Such parameters will be erased during compilation:

// TypeScript accepts this parameter
function fn(this: SomeType, x: number) {
    /* ... */
}
// Output JavaScript 
function fn(x) {
    /* ... */
}

TypeScript checks that the function call passed in this parameter is in the correct context. Instead of using the arrow function, we add a this parameter to the method definition to ensure that the method can be called correctly in a static way:

class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// report errors
const g = c.getName;
console.log(g());
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.

The trade-offs of this method are opposite to the above method using arrow function:

  • Callers of JavaScript may still call class methods by mistake without realizing it
  • Only one function is assigned to each class definition, not to each class instance
  • You can still call the methods defined by the base class through super

this type

In a class, a special type named this can dynamically reference the type of the current class. Let's see how it works:

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

Here, TypeScript infers the return value type of set as this instead of Box. Now let's create a subclass of 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 the type annotation of the parameter:

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

This is different from using other: Box -- if you have a derived class, its sameAs method will only accept other instances of the 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

You can use this is Type in the return value Type annotation of methods of classes and interfaces. When this statement is used with Type contraction (such as if statement), the Type of the target object will be contracted to the specified 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 use case for this based type protection is to allow delayed validation of specific fields. Take the following code as an example. When the hasValue is verified to be true, the undefined value in the Box can be removed:

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 constructor parameters to class properties with the same name and value. This syntax is called parameter attribute. The implementation method is to prefix the constructor parameter with one of the visibility modifiers such as public, private, protected or readonly. The final field will get these modifiers:

class Params {
    constructor(
        public readonly x: number,
        protected y: number,
        private z: number    
    ) {
        // It is not necessary to write the function body of the constructor     
    }    
}
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 expression

Background Guide: Class expression (MDN)

Class expressions are very similar to class declarations. The only difference is that class expressions do not need names, but we can still refer to them through any identifier bound to class expressions:

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

In TypeScript, classes, methods, and fields may be abstract.

Abstract methods or abstract fields have no corresponding implementation in the class. These members must exist in an abstract class that cannot be instantiated directly.

The role of an abstract class is to act as a base class and let its subclasses implement all abstract members. When a class has no abstract members, we say it is concrete.

Let's take 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.

Because Base is an abstract class, we cannot instantiate it with new. Instead, we need to create a derived class to implement abstract members:

class Derived extends Base {
    getName() {
        rteurn "world";
    }
}

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

Note that if we forget to implement the abstract members of the base class, an error will be thrown:

class Derived extends Base {
        ^
// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
  // Forget to implement abstract members
}

Abstract construction signature

Sometimes you want to take a class constructor function as a parameter and let it generate an instance of a class, and the class is derived from an abstract class.

For example, you might want to write the following code:

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

TypeScript will correctly tell you that you are trying to instantiate an abstract class. After all, according to the definition of greet, writing such code should be completely legal. It will eventually construct an instance of an abstract class:

// no way!
greet(Base);

But it actually reports an error. Therefore, the parameters accepted by the function you write should have 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 can correctly tell you which class constructor function can be called - Derived can be called because it is a concrete class, while Base cannot be called because it is an abstract class.

Connections between classes

In most cases, classes in TypeScript are structurally compared, just like other types.

For example, as like as two peas, the following two classes can replace each other, because they are exactly the same in structure.

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

Similarly, subclass relationships can exist between classes even if inheritance relationships are not explicitly declared:

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

This sounds simple and easy to understand, but there are some strange situations.

Empty class has no members. In a structured type system, a type without members is usually a superclass of any other type. So if you write an empty class (don't do that!), Then you can replace it with any type:

class Empty {}

function fn(x: Empty) {
    // No action can be performed on x, so this is not recommended
}

// These parameters can be passed in!
fn(window);
fn({});
fn(fn);

Topics: Front-end TypeScript