The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.
The translation of this article is compiled from the chapter Classes
This article does not strictly follow the original translation, but also explains and supplements part of the content.
Classes
TypeScript fully supports the class
keyword introduced by ES2015.
Like other JavaScript language features, TypeScript provides type annotations and other syntax, allowing you to express the relationship between classes and other types.
Class Members
This is the most basic class, an empty class:
class Point {}
This class is not very useful, so let's add some members.
Fields
A field declaration creates a public (public) writeable property:
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
Note: The type annotation is optional. If not specified, it will be implicitly set to any
.
Fields can be set to initial values (initializers):
class Point {
x = 0;
y = 0;
}
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
Like const
, let
and var
, the initial value of a class attribute will be used to infer its type:
const pt = new Point();
pt.x = "0";
// Type 'string' is not assignable to type 'number'.
--strictPropertyInitialization
strictPropertyInitialization The option controls whether the class field needs to be initialized in the constructor:
class BadGreeter {
name: string;
// Property 'name' has no initializer and is not definitely assigned in the constructor.
}
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
Note that the fields need to be initialized in the constructor itself. TypeScript does not analyze the methods you call in the constructor to determine the value of initialization, because a derived class may override these methods and fail to initialize members:
class BadGreeter {
name: string;
// Property 'name' has no initializer and is not definitely assigned in the constructor.
setName(): void {
this.name = '123'
}
constructor() {
this.setName();
}
}
If you insist on initializing a field by other means, rather than in the constructor (for example, importing an external library to supplement part of the class for you), you can use the definite assignment assertion operator !
:
class OKGreeter {
// Not initialized, but no error
name!: string;
}
readonly
The field can add a readonly
prefix modifier, which will prevent assignment outside of the constructor.
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
// Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
// Cannot assign to 'name' because it is a read-only property.
Constructors
The constructor of a class is very similar to a function. You can use type-annotated parameters, default values, overloads, etc.
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
But there are also some differences between the signature of the class constructor and the function signature:
- Constructor functions cannot have type parameters (for type parameters, recall the contents of generics), these belong to the outer class declaration, we will learn about it later.
- The constructor cannot have return type annotation , because it always returns the class instance type
Super Calls
Just like in JavaScript, if you have a base class, you need super()
in the constructor before this.
members.
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
// 'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Forgetting to call super
is a simple error in JavaScript, but TypeScript will alert you when needed.
Methods
The function attributes in the class are called methods. Methods are the same as functions and constructors, using the same type annotations.
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
Except for standard type annotations, TypeScript does not add anything new to methods.
Note that in a method body, it can still access fields and other methods this.
An unqualified name in the method body (unqualified name, a name that has no clearly defined scope) always points to the content in the scope of the closure.
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
// Type 'string' is not assignable to type 'number'.
}
}
Getters / Setter
Classes can also have accessors:
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
TypeScript has some special inference rules for accessors:
- If
get
exists butset
does not exist, the attribute will be automatically set toreadonly
- If the type of the setter parameter is not specified, it will be inferred as the return type of the getter
- Getters and setters must have the same member visibility ( Member Visibility ).
Since TypeScript 4.3, accessors can use different types when reading and setting.
class Thing {
_size = 0;
// 注意这里返回的是 number 类型
get size(): number {
return this._size;
}
// 注意这里允许传入的是 string | number | boolean 类型
set size(value: string | number | boolean) {
let num = Number(value);
// Don't allow NaN, Infinity, etc
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
Index Signatures
A class can declare an index signature, which is the same as the index signature of an object type:
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
check(s: string) {
return this[s] as boolean;
}
}
Because index signature types also need to capture method types, it is not easy to use these types effectively. Generally speaking, it is better to store index data elsewhere rather than in the class instance itself.
Class Heritage
JavaScript classes can inherit base classes.
implements
statement ( implements
Clauses)
You can use the implements
statement to check whether a class satisfies a specific interface
. If a class does not implement it correctly, TypeScript will report an error:
interface Pingable {
ping(): void;
}
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// Class 'Ball' incorrectly implements interface 'Pingable'.
// Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
pong() {
console.log("pong!");
}
}
The class can also implement multiple interfaces, such as class C implements A, B {
Cautions
implements
statement only checks whether the class is implemented according to the interface type, but it does not change the type of the class or the type of the method. A common mistake is to think that the implements
statement will change the type of the class-but in fact it does not:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) {
// Parameter 's' implicitly has an 'any' type.
// Notice no error here
return s.toLowercse() === "ok";
// any
}
In this example, we might think s
types are check
of name: string
parameters influence. In fact, it does not. The implements
statement does not affect how the class is checked or type inferred.
Similarly, implementing an interface with optional attributes will not create this attribute:
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
// Property 'y' does not exist on type 'C'.
extends
statement ( extends
Clauses)
The class can be a base class of extend
A derived class has all the properties and methods of the base class, and can also define additional members.
class Animal {
move() {
console.log("Moving along!");
}
}
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
Overriding attributes (Overriding Methods)
A derived class can override the fields or attributes of a base class. You can use the super
syntax to access the methods of the base class.
TypeScript mandates that the derived class is always a subtype of its base class.
For example, this is a legal way to override a method:
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
const d = new Derived();
d.greet();
d.greet("reader");
The derived class needs to follow the implementation of its base class.
And it is very common and legal to point to a derived class instance through a base class reference:
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
But what if Derived
does not follow the contract implementation of Base
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
// Make this parameter required
greet(name: string) {
// Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'.
// Type '(name: string) => void' is not assignable to type '() => void'.
console.log(`Hello, ${name.toUpperCase()}`);
}
}
Even if we ignore the incorrectly compiled code, this example will run incorrectly:
const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();
Initialization Order
In some cases, the order in which JavaScript classes are initialized can make you feel weird. Let's look at this example:
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
class Derived extends Base {
name = "derived";
}
// Prints "base", not "derived"
const d = new Derived();
What happened?
The order of class initialization is as defined in JavaScript:
- Base class field initialization
- Base class constructor running
- Derived class field initialization
- Derived class constructor running
This means that the base class constructor can only see its own name
, because the derived class field initialization has not yet run.
Inheriting Built-in Types
Note: If you do not intend to inherit the built-in types such asArray
,Error
,Map
etc. or your compilation target is ES6/ES2015 or newer, you can skip this chapter.
In ES2015, when calling super(...)
, if the constructor returns an object, it will implicitly replace the value of this
So capture super()
possible return values and use this
replace it is very necessary.
As a result, Error
like 061d45c2682362, Array
etc., may no longer behave as you expect. This is because the constructors of similar built-in objects such Error
and Array
new.target
adjust the prototype chain. However, in ECMAScript 5, when calling a constructor, there is no way to ensure the value of new.target
Other degraded compilers will have the same limitation by default.
For a subclass like the following:
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
You may find:
- The method of the object may be
undefined
, so callingsayHello
will cause an error instanceof
invalid,(new MsgError()) instanceof MsgError
will returnfalse
.
We recommend to manually adjust the prototype after calling super(...)
class MsgError extends Error {
constructor(m: string) {
super(m);
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
sayHello() {
return "hello " + this.message;
}
}
However, any MsgError
also had to manually set the prototype. If the runtime does not support Object.setPrototypeOf
, you may be able to use __proto__
.
Unfortunately, these solutions will not work properly in IE 10 or earlier versions. One solution is to manually copy the methods in the prototype to the instance (such as MsgError.prototype
to this
), but its own prototype chain is still not repaired.
Member Visibility
You can use TypeScript to control whether a method or property is visible to code outside of the class.
public
The default visibility of class members is public
, and a public
can be obtained anywhere:
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
Because public
is the default visibility modifier, you don't need to write it, unless for format or readability reasons.
protected
protected
member is only visible to subclasses:
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Exposure of protected members
Derived classes need to follow the implementation of the base class, but you can still choose to expose the base class subtypes with more capabilities. This includes making a protected
member become public
:
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
It should be noted here that if the disclosure is not intentional, in this derived class, we need to carefully copy the protected
modifier.
Cross-hierarchy protected access
Different OOP languages are controversial whether it is possible to legally obtain a protected
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Base) {
other.x = 10;
// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'.
}
}
In Java, this is legal, while C# and C++ consider this code to be illegal.
TypeScript is on the side of C# and C++. Because Derived2
of x
should only be accessed from the Derived2
, and Derived1
not one of them. In addition, if accessing x
Derived1
is illegal, accessing through a base class reference should also be illegal.
Read this "Why Can't I Access A Protected Member From A Derived Class?" , which explains more why C# does this.
private
private
a bit like protected
, but does not allow access to members, even subclasses.
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
// Property 'x' is private and only accessible within class 'Base'.
}
}
Because private
are not visible to derived classes, a derived class cannot increase its visibility:
class Base {
private x = 0;
}
class Derived extends Base {
// Class 'Derived' incorrectly extends base class 'Base'.
// Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Cross-instance private access
Different OOP languages are also inconsistent on whether different instances of a class can obtain each other's private
Things like Java, C#, C++, Swift and PHP are all allowed, but Ruby is not allowed.
TypeScript allows the acquisition of cross-instance private members:
class A {
private x = 10;
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
Warning (Caveats)
private
and protected
are only mandatory for type checking.
This means that when JavaScript is running, members like in
or simple attribute lookups can still get members of private
or protected
class MySafe {
private secretKey = 12345;
}
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);
private
allows access through square bracket syntax during type checking. This makes it easier to access the private
field during unit testing, which also makes these fields soft private rather than strictly mandatory.
class MySafe {
private secretKey = 12345;
}
const s = new MySafe();
// Not allowed during type checking
console.log(s.secretKey);
// Property 'secretKey' is private and only accessible within class 'MySafe'.
// OK
console.log(s["secretKey"]);
Unlike TypeScript's private
, JavaScript's private field ( #
) retains privacy even after compilation, and does not provide a method of obtaining square brackets like the above, which makes them hard private.
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() {}
}
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
When compiled into ES2021 or earlier, TypeScript will use WeakMaps instead of #
:
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
If you need to prevent malicious attacks and protect the values in the class, you should use strong private mechanisms such as closures, WeakMaps
, or private fields. But be aware that this will also affect performance at runtime.
TypeScript series
The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.
WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.
If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。