static
#
(private)get
(getter) and set
(setter)*
(generator)async
In this chapter, we examine how class definitions work in TypeScript:
This section is a cheat sheet for class definitions in plain JavaScript.
class OtherClass {}
class MyClass1 extends OtherClass {
= 1;
publicInstanceField
constructor() {
super();
}
publicPrototypeMethod() {
return 2;
}
}
const inst1 = new MyClass1();
.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2); assert
The next sections are about modifiers
At the end, there is a table that shows how modifiers can be combined.
static
class MyClass2 {
static staticPublicField = 1;
static staticPublicMethod() {
return 2;
}
}
.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2); assert
#
(private)class MyClass3 {
= 1;
#privateField
privateMethod() {
#return 2;
}
static accessPrivateMembers() {
// Private members can only be accessed from inside class definitions
const inst3 = new MyClass3();
.equal(inst3.#privateField, 1);
assert.equal(inst3.#privateMethod(), 2);
assert
}
}.accessPrivateMembers(); MyClass3
Warning for JavaScript:
TypeScript has been supporting private fields since version 3.8 but does not currently support private methods.
get
(getter) and set
(setter)Roughly, accessors are methods that are invoked by accessing properties. There are two kinds of accessors: getters and setters.
class MyClass5 {
= 'Rumpelstiltskin';
#name
/** Prototype getter */
name() {
get return this.#name;
}
/** Prototype setter */
name(value) {
set this.#name = value;
}
}const inst5 = new MyClass5();
.equal(inst5.name, 'Rumpelstiltskin'); // getter
assert.name = 'Queen'; // setter
inst5.equal(inst5.name, 'Queen'); // getter assert
*
(generator)class MyClass6 {
* publicPrototypeGeneratorMethod() {
yield 'hello';
yield 'world';
}
}
const inst6 = new MyClass6();
.deepEqual(
assert...inst6.publicPrototypeGeneratorMethod()],
['hello', 'world']); [
async
class MyClass7 {
async publicPrototypeAsyncMethod() {
const result = await Promise.resolve('abc');
return result + result;
}
}
const inst7 = new MyClass7();
.publicPrototypeAsyncMethod()
inst7.then(result => assert.equal(result, 'abcabc'));
const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');
class MyClass8 {
= 1;
[publicInstanceFieldKey]
[publicPrototypeMethodKey]() {return 2;
}
}
const inst8 = new MyClass8();
.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2); assert
Comments:
Symbol.iterator
. But any expression can be used inside the square brackets.Fields (no level means that a construct exists at the instance level):
Level | Visibility |
---|---|
(instance) | |
(instance) | # |
static |
|
static |
# |
Methods (no level means that a construct exists at the prototype level):
Level | Accessor | Async | Generator | Visibility |
---|---|---|---|---|
(prototype) | ||||
(prototype) | get |
|||
(prototype) | set |
|||
(prototype) | async |
|||
(prototype) | * |
|||
(prototype) | async |
* |
||
(prototype-associated) | # |
|||
(prototype-associated) | get |
# |
||
(prototype-associated) | set |
# |
||
(prototype-associated) | async |
# |
||
(prototype-associated) | * |
# |
||
(prototype-associated) | async |
* |
# |
|
static |
||||
static |
get |
|||
static |
set |
|||
static |
async |
|||
static |
* |
|||
static |
async |
* |
||
static |
# |
|||
static |
get |
# |
||
static |
set |
# |
||
static |
async |
# |
||
static |
* |
# |
||
static |
async |
* |
# |
Limitations of methods:
It’s important to keep in mind that with classes, there are two chains of prototype objects:
Consider the following plain JavaScript example:
class ClassA {
static staticMthdA() {}
constructor(instPropA) {
this.instPropA = instPropA;
}prototypeMthdA() {}
}class ClassB extends ClassA {
static staticMthdB() {}
constructor(instPropA, instPropB) {
super(instPropA);
this.instPropB = instPropB;
}prototypeMthdB() {}
}const instB = new ClassB(0, 1);
Fig. 1 shows what the prototype chains look like that are created by ClassA
and ClassB
.
By default, all data slots in TypeScript are public properties. There are two ways of keeping data private:
We’ll look at both next.
Note that TypeScript does not currently support private methods.
Private properties are a TypeScript-only (static) feature. Any property can be made private by prefixing it with the keyword private
(line A):
class PersonPrivateProperty {: string; // (A)
private nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
} }
We now get compile-time errors if we access that property in the wrong scope (line A):
= new PersonPrivateProperty('John');
const john
.equal(
assert.sayHello(), 'Hello John!');
john
// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
.name; // (A) john
However, private
doesn’t change anything at runtime. There, property .name
is indistinguishable from a public property:
.deepEqual(
assert.keys(john),
Object'name']); [
We can also see that private properties aren’t protected at runtime when we look at the JavaScript code that the class is compiled to:
class PersonPrivateProperty {
constructor(name) {
this.name = name;
}sayHello() {
return `Hello ${this.name}!`;
} }
Private fields are a new JavaScript feature that TypeScript has supported since version 3.8:
class PersonPrivateField {: string;
#nameconstructor(name: string) {
.#name = name;
this
}sayHello() {
return `Hello ${this.#name}!`;
} }
This version of Person
is mostly used the same way as the private property version:
= new PersonPrivateField('John');
const john
.equal(
assert.sayHello(), 'Hello John!'); john
However, this time, the data is completely encapsulated. Using the private field syntax outside classes is even a JavaScript syntax error. That’s why we have to use eval()
in line A so that we can execute this code:
.throws(
assert=> eval('john.#name'), // (A)
()
{: 'SyntaxError',
name: "Private field '#name' must be declared in "
message+ "an enclosing class",
;
})
.deepEqual(
assert.keys(john),
Object; [])
The compilation result is much more complicated now (slightly simplified):
var __classPrivateFieldSet = function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError(
'attempted to set private field on non-instance');
}.set(receiver, value);
privateMapreturn value;
;
}
// Omitted: __classPrivateFieldGet
var _name = new WeakMap();
class Person {
constructor(name) {
// Add an entry for this instance to _name
.set(this, void 0);
_name
// Now we can use the helper function:
__classPrivateFieldSet(this, _name, name);
}// ···
}
This code uses a common technique for keeping instance data private:
More information on this topic: see “JavaScript for impatient programmers”.
Private fields and private properties can’t be accessed in subclasses (line A):
class PrivatePerson {: string;
private nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
}
}
class PrivateEmployee extends PrivatePerson {: string;
private companyconstructor(name: string, company: string) {
super(name);
.company = company;
this
}sayHello() {
// @ts-expect-error: Property 'name' is private and only
// accessible within class 'PrivatePerson'. (2341)
return `Hello ${this.name} from ${this.company}!`; // (A)
} }
We can fix the previous example by switching from private
to protected
in line A (we also switch in line B, for consistency’s sake):
class ProtectedPerson {: string; // (A)
protected nameconstructor(name: string) {
.name = name;
this
}sayHello() {
return `Hello ${this.name}!`;
}
}
class ProtectedEmployee extends ProtectedPerson {: string; // (B)
protected companyconstructor(name: string, company: string) {
super(name);
.company = company;
this
}sayHello() {
return `Hello ${this.name} from ${this.company}!`; // OK
} }
Constructors can be private, too. That is useful when we have static factory methods and want clients to always use those methods, never the constructor directly. Static methods can access private class members, which is why the factory methods can still use the constructor.
In the following code, there is one static factory method DataContainer.create()
. It sets up instances via asynchronously loaded data. Keeping the asynchronous code in the factory method enables the actual class to be completely synchronous:
class DataContainer {: string;
#datacreate() {
static async = await Promise.resolve('downloaded'); // (A)
const data new this(data);
return
}constructor(data: string) {
private .#data = data;
this
}getData() {
'DATA: '+this.#data;
return
}
}.create()
DataContainer.then(dc => assert.equal(
.getData(), 'DATA: downloaded')); dc
In real-world code, we would use fetch()
or a similar Promise-based API to load data asynchronously in line A.
The private constructor prevents DataContainer
from being subclassed. If we want to allow subclasses, we have to make it protected
.
If the compiler setting --strictPropertyInitialization
is switched on (which is the case if we use --strict
), then TypeScript checks if all declared instance properties are correctly initialized:
Either via assignments in the constructor:
class Point {: number;
x: number;
yconstructor(x: number, y: number) {
.x = x;
this.y = y;
this
} }
Or via initializers for the property declarations:
class Point {= 0;
x = 0;
y
// No constructor needed
}
However, sometimes we initialize properties in a manner that TypeScript doesn’t recognize. Then we can use exclamation marks (definite assignment assertions) to switch off TypeScript’s warnings (line A and line B):
class Point {!: number; // (A)
x!: number; // (B)
yconstructor() {
.initProperties();
this
}initProperties() {
.x = 0;
this.y = 0;
this
} }
In the following example, we also need definite assignment assertions. Here, we set up instance properties via the constructor parameter props
:
// (A)
class CompilerError implements CompilerErrorProps { !: number;
line!: string;
descriptionconstructor(props: CompilerErrorProps) {
.assign(this, props); // (B)
Object
}
}
// Helper interface for the parameter properties
interface CompilerErrorProps {: number,
line: string,
description
}
// Using the class:
= new CompilerError({
const err : 123,
line: 'Unexpected token',
description; })
Notes:
Object.assign()
to copy the properties of parameter props
into this
.implements
ensures that the class declares all properties that are part of interface CompilerErrorProps
.public
, private
, or protected
If we use the keyword public
for a constructor parameter, then TypeScript does two things for us:
Therefore, the following two classes are equivalent:
class Point1 {constructor(public x: number, public y: number) {
}
}
class Point2 {: number;
x: number;
yconstructor(x: number, y: number) {
.x = x;
this.y = y;
this
} }
If we use private
or protected
instead of public
, then the corresponding instance properties are private or protected (not public).
Two constructs can be abstract in TypeScript:
The following code demonstrates abstract classes and methods.
On one hand, there is the abstract superclass Printable
and its helper class StringBuilder
:
class StringBuilder {= '';
string add(str: string) {
.string += str;
this
}
}abstract class Printable {
toString() {
= new StringBuilder();
const out .print(out);
this.string;
return out
}abstract print(out: StringBuilder): void;
}
On the other hand, there are the concrete subclasses Entries
and Entry
:
class Entries extends Printable {: Entry[];
entriesconstructor(entries: Entry[]) {
super();
.entries = entries;
this
}print(out: StringBuilder): void {
for (const entry of this.entries) {
.print(out);
entry
}
}
}
class Entry extends Printable {: string;
key: string;
valueconstructor(key: string, value: string) {
super();
.key = key;
this.value = value;
this
}print(out: StringBuilder): void {
.add(this.key);
out.add(': ');
out.add(this.value);
out.add('\n');
out
} }
And finally, this is us using Entries
and Entry
:
= new Entries([
const entries new Entry('accept-ranges', 'bytes'),
new Entry('content-length', '6518'),
;
]).equal(
assert.toString(),
entries'accept-ranges: bytes\ncontent-length: 6518\n');
Notes about abstract classes: