ts class description (members, inheritance, member visibility, static members, etc.)

1. Class members

1.1 The most basic class

class Point{<!-- -->}

1.2 Fields

class Point {<!-- -->
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
// Type annotation is optional, if not specified, it is implicit any

Field initialization; will run 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}`);

// Just like const, let and var, a class property's initializer will be used to infer its type
const pt2 = new Point();
pt2.x = "0"; // error: Type 'string' is not assignable to type 'number'.

– -strictPropertyInitialization
strictPropertyInitialization sets whether the control class fields need to be initialized in the constructor.

class BadGreeter {<!-- -->
  name: string; // error: Property 'name' has no initializer and is not definitely assigned in the constructor.
}
//-------------
class GoodGreeter {<!-- -->
  name: string;
 
  constructor() {<!-- -->
    this.name = "hello";
  }
}

Use explicit assignment assertion operators:

class OKGreeter {<!-- -->
name!: string; // not initialized, but no errors
}

1.3 readonly

Prevent assignment to fields outside the constructor

class Greeter {<!-- -->
readonly name: string = 'world';
constructor(otherName?: string) {<!-- -->
if(otherName !== undefined) {<!-- -->
this.name = otherName;
}
}
err() {<!-- -->
this.name = 'not ok'; // error: Cannot assign a value to 'name' because it is a read-only property
}
}
const g = new Greeter();
g.name = 'also not ok'; // error: Cannot assign a value to 'name' because it is a read-only property

1.4 Constructor

Class constructors are very similar to functions. You can add parameters with type annotations, default values, and overloads:

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
  }
}

There are just a few differences between class constructor signatures and function signatures:

  • Constructors cannot have type parameters – they belong to outer class declarations, which we will learn about later
  • Constructors cannot have return type annotations – class instance types are always returned

Super class call
Just like in JavaScript, if you have a base class, you need to call super(); in the constructor body before using any 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); // error: "super" must be called before accessing "this" in the constructor of a derived class
    super();
  }
}

1.5 Method

Functional attributes on a class are called methods. Methods can use all the same type annotations as functions and constructors:

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

Within the method body, fields and other methods must still be accessed through this. Unqualified names in a method body will always refer to something within the enclosing scope:

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"; // error: type 'string' cannot be assigned to type 'number'
  }
}

1.6 Getters/Setters

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 but no set, the property is automatically readonly
  • If the type of the setter argument is not specified, it is inferred from the getter’s return type.
  • getters and setters must have the same member visibility

Starting with TypeScript 4.3, different types of accessors can be used to get and set

class Thing {<!-- -->
  _size = 0;
 
  get size(): number {<!-- -->
    return this._size;
  }
 
  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;
  }
}

1.7 Index Signature

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

2. Class inheritance

2.1 implements

You can use the implements clause to check whether a class satisfies a specific interface. If a class fails to implement it correctly, an error is triggered:

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

Classes can also implement multiple interfaces, for example class C implements A, B {.
Notes
It is important to understand that the implements clause only checks that the class can be treated as an interface type. It doesn’t change the type of the class or its methods at all. A common source of error is assuming that the implements clause changes the class type – it doesn’t!

interface Checkable {<!-- -->
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {<!-- -->
  check(s) {<!-- -->
// Parameter 's' implicitly has type 'any'.
    return s.toLowerCase() === "ok";
  }
}

In this example, we might expect the type of s to be affected by the name: string parameter of check . It’s not – the implements clause does not change how the class body is inspected or its type is inferred.

Likewise, implementing an interface with an optional property does not create the property:

interface A {<!-- -->
  x: number;
  y?: number;
}
class C implements A {<!-- -->
  x = 0;
}
const c = new C();
c.y = 10; // error: Property 'y' does not exist on type 'C'

2.2 extends

Classes may come from base classes. A derived class has all the properties and methods of its 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);

Override method
Derived classes can also override base class fields or properties. You can access base class methods using super. syntax
TypeScript forces derived classes to always be subtypes of their base classes
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");

Type field declaration only
When target >= ES2022 or useDefineForClassFields is true , class fields are initialized after the parent class constructor completes, overriding any values set by the parent class. This can be a problem when you just want to redeclare a more accurate type for an inherited field. To handle these cases, you can write declare to indicate to TypeScript that this field declaration should have no runtime impact.

interface Animal {<!-- -->
  dateOfBirth: any;
}
 
interface Dog extends Animal {<!-- -->
  breed: any;
}
 
class AnimalHouse {<!-- -->
  resident: Animal;
  constructor(animal: Animal) {<!-- -->
    this.resident = animal;
  }
}
 
class DogHouse extends AnimalHouse {<!-- -->
  // Does not emit JavaScript code,
  // only ensures the types are correct
  declare resident: Dog;
  constructor(dog: Dog) {<!-- -->
    super(dog);
  }
}

Initialization sequence
In some cases, the order in which JavaScript classes are initialized can be surprising. Let’s consider this code:

class Base {<!-- -->
  name = "base";
  constructor() {<!-- -->
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {<!-- -->
  name = "derived";
}
 
//Print: My name is base
const d = new Derived();

The class initialization sequence defined by JavaScript is:

  • Base class fields are initialized
  • Base class constructor run
  • Derived class fields are initialized
  • Derived class constructor runs

3. Member Visibility

Use TypeScript to control whether certain methods or properties are visible to code outside the class.

3.1 public

The default visibility of class members is public. Public members can be accessed from anywhere:

class Greeter {<!-- -->
  public greet() {<!-- -->
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

3.2 protected

Protected members are visible only to subclasses of the class in which they are declared.

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(); // error: Property "getName" is protected and can only be accessed in class "Greeter" and its subclasses.

Export protected members
You can optionally expose a base class subtype with more functionality. This includes making protected members 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

Cross-level protected access

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: Derived1) {<!-- -->
    other.x = 10; // error: Property 'x' is protected and can only be accessed in class 'Derived1' and its subclasses
  }
}

3.3 private

private is like protected, but does not allow members to be accessed from subclasses:

class Base {<!-- -->
  private x = 0;
}
const b = new Base();
console.log(b.x); // error: Property "x" is a private property and can only be accessed in class "Base"

class Derived extends Base {<!-- -->
  showX() {<!-- -->
    console.log(this.x); // error: Property "x" is a private property and can only be accessed in class "Base"
  }
}

Because private members are not visible to derived classes, derived classes cannot increase their 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

class A {<!-- -->
  private x = 10;
 
  public sameAs(other: A) {<!-- -->
    // No error
    return other.x === this.x;
  }
}

Warning
private and protected are only enforced during type checking.
This means that JavaScript runtime constructs like in or simple property lookups can still access private or protected members:

class MySafe {<!-- -->
  private secretKey = 12345;
}

// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private also allows access using bracket notation during type checking. This makes private declared fields potentially easier to access, for example by unit tests, with the disadvantage that these fields are soft private and privacy is not strictly enforced.

class MySafe {<!-- -->
    private secretKey = 12345;
  }
   
  const s = new MySafe();
   
  // Not allowed during type checking
  console.log(s.secretKey); // error: The property "secretKey" is a private property and can only be accessed in class "MySafe"
   
  // OK
  console.log(s["secretKey"]);

Unlike TypeScripts’ private, JavaScript’sprivate fields (#) remain private aftercompilation and do not provide the aforementioned escape hatches like bracket notation access, which makes them very Hard to own.

class Dog {<!-- -->
  #barkAmount = 0;
  personality = "happy";
 
  constructor() {<!-- -->}
}

4. Static members

Via static declaration, accessed via 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 the same public, protected, and private visibility modifiers:

class MyClass {<!-- -->
  private static x = 0;
}
console.log(MyClass.x); // error: Property "x" is a private property and can only be accessed in class "MyClass"

Static members are also inherited:

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

5. Blocks in static classes

Static blocks allow you to write a sequence of statements with their own scope that can access private fields in the containing class. This means we can write initialization code with all the functionality of writing statements, without leaking variables, and with full access to the internal structure of our class.

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

6. Generic classes

Classes, much like interfaces, can be generic. When a generic class is instantiated using new, its type parameters are inferred in the same way 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>

Type parameters in static members

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

7. this during class runtime

class MyClass {<!-- -->
  name = "MyClass";
  getName() {<!-- -->
    return this.name;
  }
}
const c = new MyClass();
const obj = {<!-- -->
  name: "obj",
  getName: c.getName,
};
 
// Prints "obj", not "MyClass"
console.log(obj.getName());
// By default, the value of this in a function depends on how the function is called

7.1 Arrow function

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

There are some trade-offs:

  • this value is guaranteed to be correct at runtime, even for code that is not checked using TypeScript
  • This will use more memory because each class instance will have its own copy of every function defined this way
  • You cannot use super.getName in a derived class because there is no entry in the prototype chain from which to get the base class method

7.2 this parameter

In a method or function definition, the initial parameter named this has special meaning in TypeScript. These parameters are removed during compilation:

//TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {<!-- -->
  /* ... */
}
//After compilation
// JavaScript output
function fn(x) {<!-- -->
  /* ... */
}

TypeScript checks that functions with this parameters are called with the correct context. Instead of using arrow functions, we can add a this parameter to the method definition to statically force the method to be called correctly:

class MyClass {<!-- -->
  name = "MyClass";
  getName(this: MyClass) {<!-- -->
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g()); // error: "this" of type "void" The context cannot be assigned to "this" of a method of type "MyClass".

This approach offers the opposite trade-off to the arrow function approach:

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

8. this type

Within a class, a special type called this dynamically refers to the type of the current class. Let’s see what this does:

class Box {<!-- -->
  contents: string = "";
  set(value: string) {<!-- -->
  // Type of set: (method) Box.set(value: string): this
    this.contents = value;
    return this;
  }
}

Here, TypeScript infers that the return type of set is 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

You can also use this in parameter type annotations:

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

This type of protection

  • You can use this is Type in the return position of methods in classes and interfaces. When mixed with type reduction (such as an if statement), the target object’s type is reduced 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 & amp; 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 & amp; FileSystemObject
}

9. Parameter attributes

TypeScript provides special syntax for converting constructor parameters into class properties with the same name and value. These are called parameter properties and are created by preceding the constructor parameter with one of the visibility modifiers public, private, protected, or readonly. The result field gets these modifiers:

class Params {<!-- -->
  constructor(
    public readonly x: number,
    protected y: number,
    private z:number
  ) {<!-- -->
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x);
             // (property) Params.x: number
console.log(a.z); // error: Property "z" is a private property and can only be accessed in class "Params"

10. Class expression

const someClass = class<Type> {<!-- -->
  content: Type;
  constructor(value: Type) {<!-- -->
    this.content = value;
  }
};
 
const m = new someClass("Hello, world");
 // const m: someClass<string>

11. Constructor signature

JavaScript classes are instantiated using the new operator. The InstanceType utility type models this operation, given the type of the class itself.

class Point {<!-- -->
  createdAt: number;
  x: number;
  y: number
  constructor(x: number, y: number) {<!-- -->
    this.createdAt = Date.now()
    this.x = x;
    this.y = y;
  }
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {<!-- -->
  point.x + = 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8

12. abstract classes and members

Classes, methods, and fields in TypeScript may be abstract.

An abstract method or abstract field is a method for which an implementation has not yet been provided. These members must exist within the abstract class and cannot be instantiated directly.

The role of an abstract class is to serve as a base class for subclasses that implement all abstract members. A class is said to be concrete when it does not have any abstract members.

abstract class Base {<!-- -->
  abstract getName(): string;
 
  printName() {<!-- -->
    console.log("Hello, " + this.getName());
  }
}
 
const b = new Base(); // error: Unable to create an instance of abstract class

We cannot instantiate Base with new because it is abstract. Instead, we need to create a derived class and implement the abstract members:

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

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

class Derived extends Base {<!-- -->
  // Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
  // forgot to do anything
  // The non-abstract class "Derived" does not implement the abstract member "getName" inherited from the "Base" class.
}

12.1 Abstract construction signature

Sometimes you want to accept some class constructor, which produces an instance of a class derived from some abstract class.
For example, you might want to write the following code:

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

TypeScript correctly tells you that you are trying to instantiate an abstract class. After all, given the definition of greet , it’s perfectly legal to write this code, which ultimately constructs an abstract class:

// Bad!
greet(Base);

Instead, you want to write a function that accepts something with a constructor 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 correctly tells you which class constructors can be called – Derived can because it’s concrete, but Base can’t.

13. Relationship between classes

In most cases, classes in TypeScript compare structurally the same as other types.

For example, these two classes can be used interchangeably because they are identical:

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

Likewise, subtyping relationships between classes exist even without explicit inheritance:

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

It sounds simple enough, but some cases seem stranger than others.

Empty class has no members. In a structural type system, a type with no members is usually a supertype of anything else. So if you write an empty class (don’t!), anything can be used in its place:

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