Chapter 8

Classes

8

Log in to launch exercises in your editor

GitHub

Classes are a JavaScript feature that help you encapsulate data and behavior into a single unit. They are a fundamental part of object-oriented programming and are used to create objects that have properties and methods.

You can use the class keyword to define a class, and then create instances of that class using the new keyword. TypeScript adds a layer of static type checking to classes, which can help you catch errors and enforce structure in your code.

Let's build a class from scratch and see how it works.

Creating a Class

To create a class, you use the class keyword followed by the name of the class. Similar to types and interfaces, the convention is to have the name in PascalCase, which means the first letter of each word in the name is capitalized.

We'll start creating the Album class in a similar way to how a type or interface is created:

class Album {
  title: string;
Property 'title' has no initializer and is not definitely assigned in the constructor.2564
Property 'title' has no initializer and is not definitely assigned in the constructor. artist: string;
Property 'artist' has no initializer and is not definitely assigned in the constructor.2564
Property 'artist' has no initializer and is not definitely assigned in the constructor. releaseYear: number;
Property 'releaseYear' has no initializer and is not definitely assigned in the constructor.2564
Property 'releaseYear' has no initializer and is not definitely assigned in the constructor.}

At this point, even though it looks like a type or interface, TypeScript gives an error for each property in the class. How do we fix this?

Adding a Constructor

In order to fix these errors, we need to add a constructor to the class. The constructor is a special method that runs when a new instance of the class is created. It's where you can set up the initial state of the object.

To start, we'll add a constructor that assigns values for the properties of the Album class:

class Album {
  title: string;
  artist: string;
  releaseYear: number;

  constructor() {
    this.title = "Loop Finding Jazz Records";
    this.artist = "Jan Jelinek";
    this.releaseYear = 2001;
  }
}

Now, when we create a new instance of the Album class, we can access the properties and values we've set in the constructor:

const loopFindingJazzRecords = new Album();

console.log(loopFindingJazzRecords.title); // Output: Loop Finding Jazz Records

The new keyword creates a new instance of the Album class, and the constructor sets the initial values of our class's properties. In this case, because the properties are hardcoded, every instance of the Album class will have the same values.

You Don't Always Need To Type Class Properties

As we'll see, TypeScript can do some really smart inference with classes. It's able to infer the types of the properties from where we assign them in the constructor, so we can actually drop some of the type annotations:

class Album {
  title;
  artist;
  releaseYear;

  constructor() {
    this.title = "Loop Finding Jazz Records";
    this.artist = "Jan Jelinek";
    this.releaseYear = 2001;
  }
}

However, it's common to see the types specified in the class body as well since they act as a form of documentation for the class that's quick to read.

Adding Arguments to the Constructor

We can use the constructor to declare arguments for the class. This allows us to pass in values when creating a new instance of the class.

Update the constructor to accept an opts argument that includes the properties of the Album class:

// inside the Album class
constructor(opts: { title: string; artist: string; releaseYear: number }) {
 // ...
}

Then inside of the body of the constructor, we'll use assign this.title, this.artist, and this.releaseYear to the values of the opts argument.

// inside the Album class
constructor(opts: { title: string; artist: string; releaseYear: number }) {
  this.title = opts.title;
  this.artist = opts.artist;
  this.releaseYear = opts.releaseYear;
}

The this keyword refers to the instance of the class, and it's used to access the properties and methods of the class.

Now, when we create a new instance of the Album class, we can pass an object with the properties we want to set. If we don't provide any values, the default values will be used:

const loopFindingJazzRecords = new Album({
  title: "Loop Finding Jazz Records",
  artist: "Jan Jelinek",
  releaseYear: 2001,
});

console.log(loopFindingJazzRecords.title); // Output: Loop Finding Jazz Records

const unknownAlbum = new Album();

console.log(unknownAlbum.title); // Output: Unknown Album

Using a Class as a Type

An interesting property of classes in TypeScript is that they can be used as types for variables and function parameters. The syntax is similar to how you would use any other type or interface.

In this case, we'll use the Album class to type the album parameter of a printAlbumInfo function:

function printAlbumInfo(album: Album) {
  console.log(
    `${album.title} by ${album.artist}, released in ${album.releaseYear}.`,
  );
}

We can then call the function and pass in an instance of the Album class:

printAlbumInfo(sixtyNineLoveSongsAlbum);

// Output: 69 Love Songs by The Magnetic Fields, released in 1999.

While using a class as a type is possible, it's a much more common pattern to require classes to implement a specific interface.

Properties in Classes

Now that we've seen how to create a class and create new instances of it, let's look a bit closer at how properties work.

Class Property Initializers

You can set default values for properties directly in the class body. These are called class property initializers.

class Album {
  title = "Unknown Album";
  artist = "Unknown Artist";
  releaseYear = 0;
}

You can combine them with type annotations:

class Album {
  title: string = "Unknown Album";
  artist: string = "Unknown Artist";
  releaseYear: number = 0;
}

Importantly, class property initializers are resolved before the constructor is called. This means you can override the default values by assigning a different value in the constructor:

class User {
  name = "Unknown User";

  constructor() {
    this.name = "Matt Pocock";
  }
}

const user = new User();

console.log(user.name); // Output: Matt Pocock

readonly Class Properties

As we've seen with types and interfaces, the readonly keyword can be used to make a property immutable. This means that once the property is set, it cannot be changed:

class Album {
  readonly title: string;
  readonly artist: string;
  readonly releaseYear: number;
}

Optional Class Properties

We can also mark properties as optional in the same way as objects, using the ?: annotation:

class Album {
  title?: string;
  artist?: string;
  releaseYear?: number;
}

As we can see from the lack of errors above, this also means they don't need to be set in the constructor.

public and private properties

The public and private keywords are used to control the visibility and accessibility of class properties.

By default, properties are public, which means that they can be accessed from outside the class.

If we want to restrict access to certain properties, we can mark them as private. This means that they can only be accessed from within the class itself.

For example, say we want to add a rating property to the album class that will only be used inside of the class:

class Album {
  private rating = 0;
}

Now if we try to access the rating property from outside of the class, TypeScript will give us an error:

console.log(loopFindingJazzRecords.rating);
Property 'rating' is private and only accessible within class 'Album'.2341
Property 'rating' is private and only accessible within class 'Album'.

However, this doesn't actually prevent it from being accessed at runtime - private is just a compile-time annotation. You could suppress the error using a @ts-ignore (which we'll look at later) and still access the property:

// @ts-ignore
console.log(loopFindingJazzRecords.rating); // Output: 0

Runtime Private Properties

To get the same behavior at runtime, you can also use the # prefix to mark a property as private:

class Album {
  #rating = 0;
}

The # syntax behaves the same as private, but it's a newer feature that's part of the ECMAScript standard. This means that it can be used in JavaScript as well as TypeScript.

Attempting to access a #-prefixed property from outside of the class will result in a syntax error:

console.log(loopFindingJazzRecords.#rating); // SyntaxError
Property '#rating' is not accessible outside class 'Album' because it has a private identifier.18013
Property '#rating' is not accessible outside class 'Album' because it has a private identifier.

Attempting to cheat by accessing it with a dynamic string will return undefined - and still give a TypeScript error.

console.log(loopFindingJazzRecords["#rating"]); // Output: undefined
Element implicitly has an 'any' type because expression of type '"#rating"' can't be used to index type 'Album'. Property '#rating' does not exist on type 'Album'.7053
Element implicitly has an 'any' type because expression of type '"#rating"' can't be used to index type 'Album'. Property '#rating' does not exist on type 'Album'.

So, if you want to ensure that a property is truly private, you should use the # syntax.

Class Methods

Along with properties, classes can also contain methods. These functions help express the behaviors of a class and can be used to interact with both public and private properties.

Implementing Class Methods

Let's add a printAlbumInfo method to the Album class that will log the album's title, artist, and release year.

There are a couple of techniques for adding methods to a class.

The first is to follow the same pattern as the constructor and directly add the method to the class body:

// inside of the Album class
printAlbumInfo() {
  console.log(`${this.title} by ${this.artist}, released in ${this.releaseYear}.`);
}

Another option is to use an arrow function to define the method:

// inside of the Album class
printAlbumInfo = () => {
  console.log(
    `${this.title} by ${this.artist}, released in ${this.releaseYear}.`,
  );
};

Once the printAlbumInfo method has been added, we can call it to log the album's information:

loopFindingJazzRecords.printAlbumInfo();

// Output: Loop Finding Jazz Records by Jan Jelinek, released in 2001.

Arrow Functions or Class Methods?

Arrow functions and class methods do differ in their behavior. The difference is the way that this is handled.

This is runtime JavaScript behavior, so slightly outside the scope of this book. But in the interest of helpfulness, here's an example:

class MyClass {
  location = "Class";

  arrow = () => {
    console.log("arrow", this);
  };

  method() {
    console.log("method", this);
  }
}

const myObj = {
  location: "Object",
  arrow: new MyClass().arrow,
  method: new MyClass().method,
};

myObj.arrow(); // { location: 'Class' }
myObj.method(); // { location: 'Object' }

In the arrow method, this is bound to the instance of the class where it was defined. In the method method, this is bound to the object where it was called.

This can be a bit of a gotcha when working with classes, whether in JavaScript or TypeScript.

Class Inheritance

Similar to how we can extend types and interfaces, we can also extend classes in TypeScript. This allows you to create a hierarchy of classes that can inherit properties and methods from one another, making your code more organized and reusable.

For this example, we'll go back to our basic Album class that will act as our base class:

class Album {
  title: string;
  artist: string;
  releaseYear: number;

  constructor(opts: { title: string; artist: string; releaseYear: number }) {
    this.title = title;
    this.artist = artist;
    this.releaseYear = releaseYear;
  }

  displayInfo() {
    console.log(
      `${this.title} by ${this.artist}, released in ${this.releaseYear}.`,
    );
  }
}

The goal is to create a SpecialEditionAlbum class that extends the Album class and adds a bonusTracks property.

Extending a Class

The first step is to use the extends keyword to create the SpecialEditionAlbum class:

class SpecialEditionAlbum extends Album {}

Once the extends keyword is added, any new properties or methods added to the SpecialEditionAlbum class will be in addition to what it inherits from the Album class. For example, we can add a bonusTracks property to the SpecialEditionAlbum class:

class SpecialEditionAlbum extends Album {
  bonusTracks: string[];
}

Next, we need to add a constructor that includes all of the properties from the Album class as well as the bonusTracks property. There are a couple of important things to note about the constructor when extending a class.

First, the arguments to the constructor should match the shape used in the parent class. In this case, that's an opts object with the properties of the Album class along with the new bonusTracks property.

Second, we need to include a call to super(). This is a special method that calls the constructor of the parent class and sets up the properties it defines. This is crucial to ensure that the base properties are initialized properly. We'll pass in opts to the super() method and then set the bonusTracks property:

class SpecialEditionAlbum extends Album {
  bonusTracks: string[];

  constructor(opts: {
    title: string;
    artist: string;
    releaseYear: number;
    bonusTracks: string[];
  }) {
    super(opts);
    this.bonusTracks = opts.bonusTracks;
  }
}

Now that we have the SpecialEditionAlbum class set up, we can create a new instance similarly to how we would with the Album class:

const plasticOnoBandSpecialEdition = new SpecialEditionAlbum({
  title: "Plastic Ono Band",
  artist: "John Lennon",
  releaseYear: 2000,
  bonusTracks: ["Power to the People", "Do the Oz"],
});

This pattern can be used to add more methods, properties, and behavior to the SpecialEditionAlbum class, while still maintaining the properties and methods of the Album class.

protected Properties

In addition to public and private, there's a third visibility modifier called protected. This is similar to private, but it allows the property to be accessed from within classes that extend the class.

For example, if we wanted to make the title property of the Album class protected, we could do so like this:

class Album {
  protected title: string;
  // ...
}

Now, the title property can be accessed from within the SpecialEditionAlbum class, and not from outside the class.

Safe Overrides With override

You can run into trouble when extending classes if you try to override a method in a subclass. Let's say our Album class implements a displayInfo method:

class Album {
  // ...
  displayInfo() {
    console.log(
      `${this.title} by ${this.artist}, released in ${this.releaseYear}.`,
    );
  }
}

And our SpecialEditionAlbum class also implements a displayInfo method:

class SpecialEditionAlbum extends Album {
  // ...
  displayInfo() {
    console.log(
      `${this.title} by ${this.artist}, released in ${this.releaseYear}.`,
    );
    console.log(`Bonus tracks: ${this.bonusTracks.join(", ")}`);
  }
}

This overrides the displayInfo method from the Album class, adding an extra log for the bonus tracks.

But what happens if we change the displayInfo method in Album to displayAlbumInfo? SpecialEditionAlbum won't automatically get updated, and its override will no longer work.

To prevent this, you can use the override keyword in the subclass to indicate that you're intentionally overriding a method from the parent class:

class SpecialEditionAlbum extends Album {
  // ...
  override displayInfo() {
    console.log(
      `${this.title} by ${this.artist}, released in ${this.releaseYear}.`,
    );
    console.log(`Bonus tracks: ${this.bonusTracks.join(", ")}`);
  }
}

Now, if the displayInfo method in the Album class is changed, TypeScript will give an error in the SpecialEditionAlbum class, letting you know that the method is no longer being overridden.

You can also enforce this by setting noImplicitOverride to true in your tsconfig.json file. This will force you to always specify override when you're overriding a method:

{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

The implements Keyword

There are some situations where you want to enforce that a class adheres to a specific structure. To do that, you can use the implements keyword.

The SpecialEditionAlbum class we created in the previous example adds a bonusTracks property to the Album class, but there is no trackList property for the regular Album class.

Let's create an interface to enforce that any class that implements it must have a trackList property.

We'll call the interface IAlbum, and include properties for the title, artist, releaseYear, and trackList properties:

interface IAlbum {
  title: string;
  artist: string;
  releaseYear: number;
  trackList: string[];
}

Note that the I prefix is used to indicate an interface, while a T indicates a type. It isn't required to use these prefixes, but it's a common convention called Hungarian Notation and makes it more clear what the interface will be used for when reading the code. I don't recommend doing this for all your interfaces and types - only when they conflict with a class of the same name.

With the interface created, we can use the implements keyword to associate it with the Album class.

class Album implements IAlbum {
Class 'Album' incorrectly implements interface 'IAlbum'. Property 'trackList' is missing in type 'Album' but required in type 'IAlbum'.2420
Class 'Album' incorrectly implements interface 'IAlbum'. Property 'trackList' is missing in type 'Album' but required in type 'IAlbum'. title: string; artist: string; releaseYear: number; constructor(opts: { title: string; artist: string; releaseYear: number }) { this.title = opts.title; this.artist = opts.artist; this.releaseYear = opts.releaseYear; } }

Because the trackList property is missing from the Album class, TypeScript now gives us an error. In order to fix it, the trackList property needs to be added to the Album class. Once the property is added, we could update the interface or set up getters and setters accordingly:

class Album implements IAlbum {
  title: string;
  artist: string;
  releaseYear: number;
  trackList: string[];

  constructor(opts: {
    title: string;
    artist: string;
    releaseYear: number;
    trackList: string[];
  }) {
    this.title = opts.title;
    this.artist = opts.artist;
    this.releaseYear = opts.releaseYear;
    this.trackList = opts.trackList;
  }

  // ...
}

This lets us define a contract for the Album class that enforces the structure of the class and helps catch errors early.

Abstract Classes

Another pattern you can use for defining base classes is the abstract keyword. Abstract classes blur the line between types and runtime. You can declare an abstract class like this:

abstract class AlbumBase {}

You can then define methods and behavior on it, like a regular class:

abstract class AlbumBase {
  title: string;
  artist: string;
  releaseYear: number;
  trackList: string[] = [];

  constructor(opts: { title: string; artist: string; releaseYear: number }) {
    this.title = opts.title;
    this.artist = opts.artist;
    this.releaseYear = opts.releaseYear;
  }

  addTrack(track: string) {
    this.trackList.push(track);
  }
}

But if you try to create an instance of the AlbumBase class, TypeScript will give you an error:

const albumBase = new AlbumBase({
Cannot create an instance of an abstract class.2511
Cannot create an instance of an abstract class. title: "Unknown Album", artist: "Unknown Artist", releaseYear: 0, });

Instead, you'd need to create a class that extends the AlbumBase class:

class Album extends AlbumBase {
  // any extra functionality you want
}

const album = new Album({
  title: "Unknown Album",
  artist: "Unknown Artist",
  releaseYear: 0,
});

You'll notice that this idea is similar to implementing inferfaces - except that abstract classes can also include implementation details.

This means you can blur the line a little between types and runtime. You can define a type contract for a class, but make it more reusable.

Abstract Methods

On our abstract class, we can use the abstract keyword before a method to indicate that it must be implemented by any class that extends the abstract class:

abstract class AlbumBase {
  // ...other properties and methods

  abstract addReview(author: string, review: string): void;
}

Now, any class that extends AlbumBase must implement the addReview method:

class Album extends AlbumBase {
  // ...other properties and methods

  addReview(author: string, review: string) {
    // ...implementation
  }
}

This gives us another tool for expressing the structure of our classes and ensuring that they adhere to a specific contract.

Exercises

Exercise 1: Creating a Class

Here we have a class called CanvasNode that currently functions identically to an empty object:

class CanvasNode {}

Inside of a test case, we instantiate the class by calling new CanvasNode().

However, have some errors since we expect it to house two properties, specifically x and y, each with a default value of 0:

// @errors: 2339
import { it, expect } from "vitest";

class CanvasNode {}

// ---cut---
it("Should store some basic properties", () => {
  const canvasNode = new CanvasNode();

  expect(canvasNode.x).toEqual(0);
  expect(canvasNode.y).toEqual(0);

  // @ts-expect-error Property is readonly
  canvasNode.x = 10;

  // @ts-expect-error Property is readonly
  canvasNode.y = 20;
});

As seen from the @ts-expect-error directives, we also expect these properties to be readonly.

Your challenge is to implement the CanvasNode class to satisfy these requirements. For extra practice, solve the challenge with and without the use of a constructor.

Exercise 1: Creating a Class

Log in to launch exercises in your editor

GitHub

Exercise 2: Implementing Class Methods

In this exercise, we've simplified our CanvasNode class so that it no longer has read-only properties:

class CanvasNode {
  x = 0;
  y = 0;
}

There is a test case for being able to move the CanvasNode object to a new location:

it("Should be able to move to a new location", () => {
  const canvasNode = new CanvasNode();

  expect(canvasNode.x).toEqual(0);
  expect(canvasNode.y).toEqual(0);

  canvasNode.move(10, 20);
Property 'move' does not exist on type 'CanvasNode'.2339
Property 'move' does not exist on type 'CanvasNode'. expect(canvasNode.x).toEqual(10); expect(canvasNode.y).toEqual(20); });

Currently, there is an error under the move method call because the CanvasNode class does not have a move method.

Your task is to add a move method to the CanvasNode class that will update the x and y properties to the new location.

Exercise 2: Implementing Class Methods

Log in to launch exercises in your editor

GitHub

Exercise 3: Implement a Getter

Let's continue working with the CanvasNode class, which now has a constructor that accepts an optional argument, renamed to position. This position is an object that replaces the individual x and y we had before:

class CanvasNode {
  x: number;
  y: number;

  constructor(position?: { x: number; y: number }) {
    this.x = position?.x ?? 0;
    this.y = position?.y ?? 0;
  }

  move(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

In these test cases, there are errors accessing the position property since it is not currently a property of the CanvasNode class:

it("Should be able to move", () => {
  const canvasNode = new CanvasNode();

  expect(canvasNode.position).toEqual({ x: 0, y: 0 });
Property 'position' does not exist on type 'CanvasNode'.2339
Property 'position' does not exist on type 'CanvasNode'. canvasNode.move(10, 20); expect(canvasNode.position).toEqual({ x: 10, y: 20 });
Property 'position' does not exist on type 'CanvasNode'.2339
Property 'position' does not exist on type 'CanvasNode'.}); it("Should be able to receive an initial position", () => { const canvasNode = new CanvasNode({ x: 10, y: 20, }); expect(canvasNode.position).toEqual({ x: 10, y: 20 });
Property 'position' does not exist on type 'CanvasNode'.2339
Property 'position' does not exist on type 'CanvasNode'.});

Your task is to update the CanvasNode class to include a position getter that will allow for the test cases to pass.

Exercise 3: Implement a Getter

Log in to launch exercises in your editor

GitHub

Exercise 4: Implement a Setter

The CanvasNode class has been updated so that x and y are now private properties:

class CanvasNode {
  #x: number;
  #y: number;

  constructor(position?: { x: number; y: number }) {
    this.#x = position?.x ?? 0;
    this.#y = position?.y ?? 0;
  }

  // your `position` getter method here

  // move method as before
}

The # in front of the x and y properties means they are readonly and can't be modified directly outside of the class. In addition, when a getter is present without a setter, its property will also be treated as readonly, as seen in this test case:

canvasNode.position = { x: 10, y: 20 };
Cannot assign to 'position' because it is a read-only property.2540
Cannot assign to 'position' because it is a read-only property.

Your task is to write a setter for the position property that will allow for the test case to pass.

Exercise 4: Implement a Setter

Log in to launch exercises in your editor

GitHub

Exercise 5: Extending a Class

Here we have a more complex version of the CanvasNode class.

In addition to the x and y properties, the class now has a viewMode property that is typed as ViewMode which can be set to hidden, visible, or selected:

type ViewMode = "hidden" | "visible" | "selected";

class CanvasNode {
  x = 0;
  y = 0;
  viewMode: ViewMode = "visible";

  constructor(options?: { x: number; y: number; viewMode?: ViewMode }) {
    this.x = options?.x ?? 0;
    this.y = options?.y ?? 0;
    this.viewMode = options?.viewMode ?? "visible";
  }

  /* getter, setter, and move methods as before */

Imagine if our application had a Shape class that only needed the x and y properties and the ability to move around. It wouldn't need the viewMode property or the logic related to it.

Your task is to refactor the CanvasNode class to split the x and y properties into a separate class called Shape. Then, the CanvasNode class should extend the Shape class, adding the viewMode property and the logic related to it.

If you like, you can use an abstract class to define Shape.

Exercise 5: Extending a Class

Log in to launch exercises in your editor

GitHub

Solution 1: Creating a Class

Here's an example of a CanvasNode class with a constructor that meets the requirements:

class CanvasNode {
  readonly x: number;
  readonly y: number;

  constructor() {
    this.x = 0;
    this.y = 0;
  }
}

Without a constructor, the CanvasNode class can be implemented by assigning the properties directly:

class CanvasNode {
  readonly x = 0;
  readonly y = 0;
}

Solution 2: Implementing Class Methods

The move method can be implemented either as a regular method or as an arrow function:

Here's the regular method:

class CanvasNode {
  x = 0;
  y = 0;

  move(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

And the arrow function:

class CanvasNode {
  x = 0;
  y = 0;

  move = (x: number, y: number) => {
    this.x = x;
    this.y = y;
  };
}

As discussed in a previous section, it's safer to use the arrow function to avoid issues with this.

Solution 3: Implement a Getter

Here's how the CanvasNode class can be updated to include a getter for the position property:

class CanvasNode {
  x: number;
  y: number;

  constructor(position?: { x: number; y: number }) {
    this.x = position?.x ?? 0;
    this.y = position?.y ?? 0;
  }

  move(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  get position() {
    return { x: this.x, y: this.y };
  }
}

With the getter in place, the test cases will pass.

Remember, when using a getter, you can access the property as if it were a regular property on the class instance:

const canvasNode = new CanvasNode();
console.log(canvasNode.position.x); // 0
console.log(canvasNode.position.y); // 0

Solution 4: Implement a Setter

Here's how a position setter can be added to the CanvasNode class:

class CanvasNode {
  // inside the CanvasNode class
  set position(pos) {
    this.x = pos.x;
    this.y = pos.y;
  }
}

Note that we don't have to add a type to the pos parameter since TypeScript is smart enough to infer it based on the getter's return type.

Solution 5: Extending a Class

The new Shape class would look very similar to the original CanvasNode class:

class Shape {
  #x: number;
  #y: number;

  constructor(options?: { x: number; y: number }) {
    this.#x = options?.x ?? 0;
    this.#y = options?.y ?? 0;
  }

  // position getter and setter methods

  move(x: number, y: number) {
    this.#x = x;
    this.#y = y;
  }
}

The CanvasNode class would then extend the Shape class and add the viewMode property. The constructor would also be updated to accept the viewMode and call super() to pass the x and y properties to the Shape class:

class CanvasNode extends Shape {
  #viewMode: ViewMode;

  constructor(options?: { x: number; y: number; viewMode?: ViewMode }) {
    super(options);
    this.#viewMode = options?.viewMode ?? "visible";
  }
}
PreviousMutability
NextTypeScript-only Features