- Note: There are currently no Chinese translations of the latest TypeScript official documentation available online, so there is a translation plan. Since I’m also a beginner to TypeScript, I can’t guarantee 100% accuracy in TypeScript translations, so I feel free to point out any errors in the comments section.
- Translation content: The temporary translation content is TypeScript Handbook, and other parts of the translation document will be supplemented later.
- Project address: typescript-doc-zh, if it helps you, you can click a star ~
The official document address of this chapter is Classes
Class (MDN)
class
TypeScript provides full support for the class keyword introduced in ES2015.
Like other JavaScript language features, TypeScript provides type annotations and other syntax for classes to help developers express relationships between classes and other types.
Members of the class
This is the most basic class — it is empty:
class Point {}
Copy the code
This class is currently useless, so let’s add some members to it.
field
Declaring a field adds a common, writable property to the class:
class Point {
x: number;
y: number;
}
const pt = new Point()
pt.x = 0;
pt.y = 0;
Copy the code
As with the other features, type annotations are optional, but if no type is specified, the any type is implicitly used.
Fields can also be initialized, which happens automatically when the class is instantiated:
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}.${pt.y}`);
Copy the code
As with const, let, and var, initialization statements for class attributes are used for type inference:
const pt = new Point();
pt.x = "0";
// Type 'string' is not assignable to type 'number'.
Copy the code
–strictPropertyInitialization
Whether the configuration items strictPropertyInitialization is used to control the class of the field 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"; }}Copy the code
Note that the fields need to be initialized within the constructor itself. TypeScript does not analyze methods called in the constructor to detect initialization statements, because derived classes may override these methods, causing members to fail to be initialized.
If you insist on initializing a field using methods other than the constructor (such as populating the class’s contents with an external library), you can use the determinate assignment assertion operator instead! :
class OKGreeter {
// No initialization, but no errorname! :string;
}
Copy the code
readonly
Fields can be prefixed with readonly modifiers to prevent field 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
The constructor
Class constructors are much like functions. You can add type annotations to their arguments, use parameter defaults, or use function overloading:
class Point {
x: number;
y: number;
// A normal signature with the default value of the parameter is used
constructor(x = 0, y = 0) {
this.x = x;
this.y = y; }}class Point {
// Use an overload
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y? :any) {
// TBD}}Copy the code
The constructor signature of a class differs from the function signature only in one way:
- Constructors cannot use type parameters — they are part of the class declaration, which we’ll learn about later
- The constructor cannot add a type annotation to a return value — the type it returns is always the type of the class instance
super
call
Just like JavaScript, if you have a base class and a derived class, use this. Before accessing a class member, you must call super() in the constructor; :
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// An error value is printed in ES5 and an error is reported 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 common mistake in JavaScript, but TypeScript will remind you when necessary.
methods
The property of a class may be a function, in which case we call it a method. Methods, like functions and constructors, can use various types of annotations:
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 beyond standard type annotations.
Note that in the method body, you must access the fields and other methods of the class through this. Using an invalid name in a method body will be treated as accessing a variable in a neighboring scope:
let x: number = 0;
class C {
x: string = "hello";
m() {
// The following sentence is an attempt to modify the first line of x, not the class attribute
x = "world";
^
// Type 'string' is not assignable to type 'number'.}}Copy the code
Getters/Setters
Classes can also have accessors:
class C {
_length = 0;
get length() {return this._length;
}
set length(value) {this._length = value; }}Copy the code
Note: In JavaScript, a get/set pair without extra logic is useless. If you don’t need to add additional logic when performing a GET /set operation, you simply expose the field as a public field.
TypeScript has some special inference rules for accessors:
- if
get
There is,set
If not, the property automatically becomes read-only - If the type of the setter parameter is not specified, the parameter type is inferred based on the type of the value returned by the getter
- Getters and setters must have the same member visibility.
Starting with TypeScript 4.3, getters and setters for 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; }}Copy the code
The index sign
Classes can declare index signatures, which work like index signatures of other 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 in a different 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 particular 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!"); }}Copy the code
Classes can implement multiple interfaces, such as class C implements A,B {.
Matters needing attention
It’s important to understand that the implements clause only checks if a class can be treated as an interface type, and it doesn’t change the class’s type or its methods at all. A common mistake is to think that the implements clause changes the type of a class — it doesn’t!
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}}Copy the code
In this case, we might expect the type of S to be affected by the name: string parameter of check in the interface. But it doesn’t — the implements clause doesn’t have any effect on class body checking or type inference.
Similarly, implementing an interface with an optional attribute does not create that 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
extends
clause
Classes can inherit from 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 methods
d.move();
// Derived class methods
d.woof(3);
Copy the code
Overriding methods
Derived classes can also override fields or attributes of base classes. You can access the methods of the base class using the super. syntax. Note that since a JavaScript class is just a simple lookup object, there is no concept of a “superclass field.”
TypeScript enforces that a derived class is always a subclass of the base class.
For example, here is an example of a valid override 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
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 by a base class reference. An approach to:
// A derived class instance is named by a base class reference
const b: Base = d;
// No problem
b.greet();
Copy the code
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 mandatory
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
If you ignore the error and compile the code, the following code will execute with an error:
const b: Base = new Derived();
// Error because name is undefined
b.greet();
Copy the code
Initialization sequence
The order in which JavaScript classes are initialized 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();
Copy the code
What’s going on here?
According to JavaScript, classes are initialized in the following order:
- Initializes the fields of the base class
- Executes the constructor for the base class
- Initializes the fields of a derived class
- Executes the constructor of a derived class
This means that because the fields of the derived class are not initialized when the base constructor executes, the base constructor can only see its own name value.
Inheriting built-in types
Note: If you do not intend to inherit built-in types such as Array, Error, Map, etc., or if your build target is explicitly set to ES6/ES2015 or higher, you can skip this section.
In ES2015, the constructor returning the instance object implicitly replaces the value of this with super(…). Any caller of. It is necessary to have the generated constructor code capture super(…) , and replace it with this.
Therefore, subclasses of Error, Array, and so on May not work as expected. This is because constructors such as Error and Array use ES6’s new.target to adjust the prototype chain, but there is no similar way to ensure the value of new.target when the constructor is called in ES5. By default, other underlying compilers usually have the same limitation.
For a subclass like the following:
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message; }}Copy the code
You might find:
- The instance object returned after calling a subclass, whose method might be
undefined
, so callsayHello
An error will be thrown - Between a subclass instance and a subclass
instanceof
It could be damaged, so(new MsgError()) instanceof MsgError
Will returnfalse
.
The recommended way to do this is in any super(…) Manually adjust the prototype chain after the call:
class MsgError extends Error {
constructor(m: string) {
super(m);
// Explicitly set the prototype chain
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message; }}Copy the code
However, any subclass of MsgError also needs to set the prototype manually. For runtimes that do not support Object.setPrototypeOf, you can use __proto__ instead.
Unfortunately, these workarounds don’t work on IE10 or older versions. You can manually copy methods from prototypes to instances (such as msgeror.Prototype to this), but the prototype chain itself cannot be fixed.
Member visibility
You can use TypeScript to control whether specific methods or properties are visible outside of a class.
public
The default visibility of class members is public. Public members can be accessed anywhere:
class Greeter {
public greet(){
console.log('hi! '); }}const g = new Greeter();
g.greet();
Copy the code
Since members’ visibility is public by default, you don’t need to declare them explicitly in front of class members, but you can do so for code specification or readability reasons.
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.
Copy the code
Expose protected members
A derived class needs to follow the constraints of its base class, but can choose to expose subclasses of the base class that have more functionality. This includes making protected members public:
class Base {
protected m = 10;
}
class Derived extends Base {
// There are no modifiers, so default visibility is public
m = 15;
}
const d = new Dervied();
console.log(d.m); // OK
Copy the code
Note that Dervied can already read and write member M freely, so this does not change the “security” of the situation. The important thing 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.
Access protected members across hierarchies
Different OOP languages dispute whether it is legal to access 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
For example, Java considers the above code legal, but C# and C++ consider it illegal.
TypeScript also considers this illegal because it is only legal to access Derived2’s X in a subclass of Derived2, which Derived1 isn’t. Furthermore, it is already illegal to access X through a Derived1 reference (which it should be!). It should also be illegal to access it through a base class reference.
For more information on why C# considers this code 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 are not accessible even in subclasses:
class Base {
private x = 0;
}
const b = new Base();
// Cannot be accessed outside the class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
showX() {
// Cannot be accessed in subclasses
console.log(this.x);
^
// Property 'x' is private and only accessible within class 'Base'.}}Copy the code
Since private members are not visible to derived classes, derived classes cannot improve their visibility:
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;
}
Copy the code
Access 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 considers it illegal.
TypeScript allows private members to be accessed across instances:
class A {
private x = 10;
public sameAs(other: A) {
// No error will be reported
return other.x === this.x; }}Copy the code
Matters needing attention
As with everything else in the TypeScript type system, private and protected only take effect during type checking.
This means that JavaScript runtime operations such as in or simple property lookups can still access private or protected members:
class MySafe {
private serectKey = 123345;
}
// Print 12345 in the JavaScript file
const s = new MySafe();
console.log(s.secretKey);
Copy the code
Even during type checking, private members can be accessed using square brackets syntax. Therefore, when performing operations such as unit tests, it is easier to access private fields, but the disadvantage is that these fields are “weakly private” and cannot be strictly private.
class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
// Such access to private members is not allowed during type checking
console.log(s.secretKey);
^
// Property 'secretKey' is private and only accessible within class 'MySafe'.
// But it can be accessed by square bracket syntax
console.log(s["secretKey"]);
Copy the code
Unlike TypeScript’s private declaration of private members, JavaScript’s # declaration of private fields remains private after compilation and does not provide square bracket syntax for accessing private members like above. So private members of JavaScript are “strongly private.”
class Dog {
#barkAmount = 0;
personality = 'happy';
constructor(){}}Copy the code
Take this TypeScript code as an example:
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor(){}}Copy the code
When compiled to ES2021 or lower, TypeScript uses WeakMap 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 protect a value in a class from malicious modification, you should use a mechanism that provides runtime privacy protection, such as closures, WeakMaps, or private fields. Note that these private checks added at run time can affect performance.
Static members
Static Member (MDN)
Classes can have static members. These members are independent of a particular instance of the class and can be accessed through the class constructor object itself:
class MyClass {
static x = 0;
static printX(){
console.log(MyClass.x); }}console.log(MyClass.x);
MyClass.printX();
Copy the code
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'.
Copy the code
Static members can also be inherited:
class Base {
static getGreeting() {
return "Hello world"; }}class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Copy the code
Special static member name
Overwriting the properties of a Function prototype is usually unsafe/impossible. Because the class itself is also a function that can be called through new, you can’t use specific static member names. Function attributes such as name, length, and call cannot be used as static member names:
class S {
static name = 'S! ';
^
// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
Copy the code
Why are there no static classes?
TypeScript (and JavaScript) don’t provide static classes like C# and Java do.
C# and Java require static classes because these languages require that all data and functions be placed in a single class. Because there is no such limitation in TypeScirpt, there is no need for static classes. Classes that have a single instance are typically represented in JavaScript/TypeScirpt as a normal object.
For example, we don’t need the “static class” syntax in TypeScript, because a regular object (even a top-level function) can do the same thing:
// Unnecessary static classes
class MyStaticClass {
static doSomething(){}}// First choice (option 1)
function doSomething() {}
// First choice (option 2)
const MyHelperObject = {
dosomething(){}};Copy the code
A static block in a class
Static blocks allow you to write a series of declarations that have their own scope and access private fields in the containing class. This means that we can write initialization code that contains declarations, has no problem with variable leakage, and has full access to 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{}}}Copy the code
A generic class
Classes, like interfaces, can use generics. When a generic class is instantiated with new, its type arguments are inferred just as in a function call:
class Box<Type> {
contents: Type;
constructor(value: Type){
this.contents = value; }}const b = new Box('hello! ');
^
// const b: Box<string>
Copy the code
Classes can use generic constraints and default values just like interfaces.
Type parameters in static members
The following code is illegal, but the reasons may not be obvious:
class Box<Type> {
static defaultValue: Type;
^
// Static members cannot reference class type parameters.
}
Copy the code
Remember, types are always completely erased after compilation! At run time, there is only one slot for the Box. DefaultValue property. This means that setting Box
. DefaultValue (if it can be set) will also change Box
. DefaultValue — which won’t work. Static members of a generic class can never reference the class’s type parameters.
Class runtimethis
It’s important to remember that TypeScript doesn’t change the runtime behavior of JavaScript. JavaScript is known to have some special runtime behavior.
JavaScript’s handling of this is indeed 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());
Copy the code
To make a long story short, by default, the value of this in a function depends on how the function is called. In this case, since we are calling the function via an obj reference, its this value is obj, not the class instance.
This is usually not what we expect! TypeScript provides ways to reduce or prevent such errors.
Arrow function
If your function often loses the this context when called, it is best 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());
Copy the code
There are some trade-offs:
- This is guaranteed at run time
this
Is correct, even for code that isn’t checked with TypeScript - This takes up more memory, because functions defined this way result in a copy of the function for each class instance
- You can’t use it in derived classes
super.getName
Because there is no entry in the prototype chain to get the base class methods
this
parameter
In TypeScript method or function definitions, the name of the first parameter has a special meaning if it is this. Arguments like this are erased at compile time:
// TypeScript accepts this
function fn(this: SomeType, x: number) {
/ *... * /
}
// Output JavaScript
function fn(x) {
/ *... * /
}
Copy the code
TypeScript checks that the function call passed this is in the correct context. Instead of using the arrow function, we add a this parameter to the method definition to statically ensure that the method is called correctly:
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name; }}const c = new MyClass();
// OK
c.getName();
/ / an error
const g = c.getName;
console.log(g());
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
Copy the code
The tradeoff for this approach is the opposite of the arrow function used above:
- JavaScript callers may still incorrectly call class methods without realizing it
- Only one function is assigned to each class definition, not to each class instance
- Can still get through
super
Calls the methods defined by the base class
this
type
Within 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; }}Copy the code
Here, TypeScript extrapolates the return value type of set to this, not 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
Copy the code
You can also use this in the parameter type annotation:
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content; }}Copy the code
This is not the sameAs using other: Box — if you have a derived class, its sameAs method will only accept other instances of that 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'. */
Copy the code
Based on thethis
Type protection of
You can use this is Type in the return value Type annotations of class and interface methods. When used in conjunction with Type contraction (such as the if statement), the target object’s Type is shrunk 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
}
Copy the code
A common use case for type protection based on this is to allow delayed validation of a particular field. For example, if hasValue is true, the value of undefined can be removed from the Box:
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
.value? : unknown
if (box.hasValue()) {
box.value;
^
// (property) value: unknown
}
Copy the code
Parameter properties
TypeScript provides a special syntax for converting constructor parameters to class attributes with the same name and value. This syntax is called parameter properties and is implemented by prefixing constructor arguments with one of the visibility modifiers 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
) {
// There is no need to write the 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'.
Copy the code
Such expressions
Class Expression (MDN)
Class expressions and class declarations are very similar. The only difference is that class expressions don’t need names, but we can still refer to them by any identifier we bind 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>
Copy the code
Abstract classes and members
In TypeScript, classes, methods, and fields can be abstract.
Abstract methods or abstract fields have no corresponding implementation in a 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 the abstract members. A class is concrete when it does not have any abstract members.
Here’s 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.
Copy the code
Because Base is an abstract class, we cannot instantiate it using new. Instead, we need to create a derived class that implements the abstract member:
class Derived extends Base {
getName() {
rteurn "world"; }}const d = new Derived();
d.printName();
Copy the code
Note that if we forget to implement an abstract member of the base class, an error is thrown:
class Derived extends Base {^// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
// Forget to implement the abstract member
}
Copy the code
Abstract construction signature
Sometimes you want to take a class constructor function as an argument that produces an instance of a class derived from an abstract class.
For example, you might want to write code like this:
function greet(ctor: typeof Base) {
const instance = new ctor();
// Cannot create an instance of an abstract class.
instance.printName();
}
Copy the code
TypeScript correctly tells you that you are trying to instantiate an abstract class. After all, according to greet, it should be perfectly legal to write code that will eventually construct an instance of the abstract class:
/ / no way!
greet(Base);
Copy the code
But it actually reports an error. So, you should write functions that take arguments with 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. */
Copy the code
TypeScript now correctly tells you which class constructor functions can be called — Derived can be called because it’s a concrete class, and Base can’t be called because it’s abstract.
Relationships between classes
For the most part, classes in TypeScript are compared structurally, just like any other type.
For example, the following two classes can substitute for each other because they are structurally identical:
class Point1 {
x = 0;
y = 0;
}
class Point2 {
x = 0;
y = 0;
}
// OK
const p: Point1 = new Point2();
Copy the code
Similarly, there can be a subclass relationship between classes without explicitly declaring an inheritance relationship:
class Person {
name: string;
age: number;
}
class Employee {
name: string;
age: number;
salary: number;
}
// OK
const p: Person = new Employee();
Copy the code
That sounds straightforward, but there are other situations that are a little bit more bizarre.
An empty class has no members. In a structured type system, a type with no 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) {
// You can't do anything with x, so it's not recommended
}
// These parameters can be passed in!
fn(window);
fn({});
fn(fn);
Copy the code