Typing classes
A class declaration produces two things at once: a type describing its instances, and a factory for making them. Everything the checker contributes to a class body is one of three things, a visibility rule, a promise about initialization, or a contract with an interface, and all three are compile-time only. private, protected, abstract, implements, and override leave nothing behind. The parts that do survive are JavaScript's: # private fields, which the private class members page covers, and extends, which the subclassing page covers.
Create programs/typing-classes.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import {
assert,
assertEquals,
assertFalse,
assertStrictEquals,
assertThrows,
} from "@std/assert";
Below the import, add the interface and class most of the page revisits:
interface Priced {
readonly sku: string;
total(quantity: number): number;
}
class Product implements Priced {
readonly sku: string;
protected unitPrice: number;
constructor(sku: string, unitPrice: number) {
this.sku = sku;
this.unitPrice = unitPrice;
}
total(quantity: number): number {
return this.unitPrice * quantity;
}
get description(): string {
return `${this.sku} at ${this.unitPrice}`;
}
}
Follow the page as you add and revise the runnable examples below it.
a class declares a type and a factory
Deno.test("a class declares a type and a factory", () => {
const carrot = new Product("veg-1", 40);
assertStrictEquals(carrot.total(3), 120);
assertStrictEquals(carrot.description, "veg-1 at 40");
const asInterface: Priced = carrot;
assertStrictEquals(asInterface.sku, "veg-1");
});
Check programs/typing-classes.test.ts
running 1 test from ./programs/typing-classes.test.ts
a class declares a type and a factory ... ok (197µs)
ok | 1 passed | 0 failed (1ms)
Fields are declared with their types before the constructor assigns them, and methods and accessors are annotated like functions. implements Priced states that this class is meant to satisfy that interface, and protected says unitPrice is for this class and its subclasses. readonly is the modifier from the read-only page and protected is the cousin of the private the private class members page measured, and neither means anything at run time.
parameter properties declare and assign at once
Deno.test("parameter properties declare and assign at once", () => {
class Discounted extends Product implements Priced {
constructor(sku: string, unitPrice: number, private percent: number) {
super(sku, unitPrice);
}
override total(quantity: number): number {
return super.total(quantity) * (1 - this.percent / 100);
}
}
const carrot = new Discounted("veg-1", 40, 50);
assertStrictEquals(carrot.total(2), 40);
assertEquals(Object.keys(carrot), ["sku", "unitPrice", "percent"]);
});
parameter properties declare and assign at once ... ok (473µs)
A modifier on a constructor parameter declares an instance property and assigns the argument to it. Three words instead of three lines, and the property really is there: Object.keys lists percent beside the two fields the base class assigned by hand. This is the one feature in this entry that is not just erased type information, because it emits code, which a later step returns to.
an abstract class is a template with a hole in it
Add two more module-scope pieces below Product, a tiny builder and an abstract base:
class LineBuffer {
text = "";
append(part: string): void {
this.text += part;
}
}
abstract class Renderable {
abstract render(out: LineBuffer): void;
toString(): string {
const out = new LineBuffer();
this.render(out);
return out.text;
}
}
Deno.test("an abstract class is a template with a hole in it", () => {
class Header extends Renderable {
constructor(private title: string) {
super();
}
override render(out: LineBuffer): void {
out.append(`# ${this.title}`);
}
}
assertStrictEquals(String(new Header("Types")), "# Types");
});
an abstract class is a template with a hole in it ... ok (47µs)
abstract on a class means it cannot be instantiated, and abstract on a member means it has a type and no implementation, which every concrete subclass must supply. Together they let a base class write an algorithm in terms of a step it does not have, which is what toString does here. Note that implementing an abstract member does not require override, even though Deno has noImplicitOverride on, the flag the subclassing page captured, because there was nothing to override: the base declared a type and no behaviour. Using override there anyway is allowed and reads well, which is why Header does. The checker enforces both halves, along with the two implements refusals of the later steps, so collect all four in a scratch file programs/contracts.ts:
interface Priced {
readonly sku: string;
total(quantity: number): number;
}
export class Incomplete implements Priced {
readonly sku = "veg-1";
}
export class Hidden implements Priced {
private sku = "veg-1";
total(quantity: number): number {
return quantity;
}
}
export abstract class Partial implements Priced {
readonly sku = "veg-1";
abstract total(quantity: number): number;
}
abstract class Renderable {
abstract render(): string;
}
export const nothing = new Renderable();
export class Silent extends Renderable {}
Check programs/contracts.ts
TS2420 [ERROR]: Class 'Incomplete' incorrectly implements interface 'Priced'.
Property 'total' is missing in type 'Incomplete' but required in type 'Priced'.
export class Incomplete implements Priced {
~~~~~~~~~~
at file:///programs/contracts.ts:6:14
'total' is declared here.
total(quantity: number): number;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
at file:///programs/contracts.ts:3:3
TS2420 [ERROR]: Class 'Hidden' incorrectly implements interface 'Priced'.
Property 'sku' is private in type 'Hidden' but not in type 'Priced'.
export class Hidden implements Priced {
~~~~~~
at file:///programs/contracts.ts:10:14
TS2511 [ERROR]: Cannot create an instance of an abstract class.
export const nothing = new Renderable();
~~~~~~~~~~~~~~~
at file:///programs/contracts.ts:28:24
TS2515 [ERROR]: Non-abstract class 'Silent' does not implement inherited abstract member render from class 'Renderable'.
export class Silent extends Renderable {}
~~~~~~
at file:///programs/contracts.ts:30:14
Found 4 errors.
error: Type checking failed.
Four refusals and one silence worth noticing: Partial draws no complaint, because an abstract class may sign a contract it has not fulfilled and pass the obligation down, which is the useful combination, implements for the promise and abstract for the part each subclass answers differently. The two TS2420s belong to a later step; the second one already says something important, that a private member is not part of the class's public shape, so it cannot satisfy a promise made about that shape. If you want the field private and the property public, use a # field and an accessor. Delete the scratch file.
abstractness is a compile-time fact and nothing else
Deno.test("abstractness is a compile-time fact and nothing else", () => {
const Sneaky = Renderable as unknown as new () => Renderable;
const instance = new Sneaky();
assert(instance instanceof Renderable);
assertFalse("render" in instance);
assertThrows(
() => String(instance),
TypeError,
"this.render is not a function",
);
});
abstractness is a compile-time fact and nothing else ... ok (577µs)
There is no such thing as an abstract class at run time. Renderable compiles to an ordinary class, new on it works the moment the checker is persuaded to look away, and the resulting object is missing the method the base class calls, so nothing throws until toString reaches for render. That is the shape of every erased feature in this entry, and it is worth seeing once rather than being told five times: the checker is the only thing enforcing these rules, so they hold exactly as far as checked code reaches.
implements checks the class and does not change it
Deno.test("implements checks the class and does not change it", () => {
class Extra implements Priced {
readonly sku = "veg-1";
audit = 0;
total(quantity: number): number {
this.audit++;
return quantity;
}
}
const product = new Extra();
assertStrictEquals(product.total(2), 2);
assertStrictEquals(product.audit, 1);
});
implements checks the class and does not change it ... ok (39µs)
implements is a claim to be verified, not a lens to look through. Extra has a member the interface never mentioned, and callers can still see it, because the class's type is what the class declared; compare a variable annotated Priced, which really does hide audit. The two failure modes were already captured in the contracts scratch: a missing member, and the less obvious one, a member that exists and is hidden by private.
the definite assignment assertion is a promise you make
strictPropertyInitialization comes with strict, so a declared field must be assigned in the constructor or given an initializer. Put the reasonable-looking violation in a scratch file programs/initialization.ts:
export class Point {
x: number;
y: number;
reset(): void {
this.x = 0;
this.y = 0;
}
}
Check programs/initialization.ts
TS2564 [ERROR]: Property 'x' has no initializer and is not definitely assigned in the constructor.
x: number;
^
at file:///programs/initialization.ts:2:3
TS2564 [ERROR]: Property 'y' has no initializer and is not definitely assigned in the constructor.
y: number;
^
at file:///programs/initialization.ts:3:3
Found 2 errors.
error: Type checking failed.
The checker is right to complain and wrong about the code: reset assigns both, and the analysis only follows the constructor. An exclamation mark says I-have-checked-you-have-not. Delete the scratch file, and pin both the fix and the bulk-assignment pairing that makes the escape worth having:
Deno.test("the definite assignment assertion is a promise you make", () => {
class LazyPoint {
x!: number;
y!: number;
constructor() {
this.reset();
}
reset(): void {
this.x = 0;
this.y = 0;
}
}
assertStrictEquals(new LazyPoint().x, 0);
interface CompilerErrorProps {
line: number;
description: string;
}
class CompilerError implements CompilerErrorProps {
line!: number;
description!: string;
constructor(props: CompilerErrorProps) {
Object.assign(this, props);
}
}
const error = new CompilerError({
line: 12,
description: "Unexpected token",
});
assertStrictEquals(error.line, 12);
assertStrictEquals(error.description, "Unexpected token");
});
the definite assignment assertion is a promise you make ... ok (44µs)
Object.assign copies every property in one line and tells the checker nothing, so the implements CompilerErrorProps is what keeps the class honest: add a field to the props interface and the class fails to compile until it declares it too. The two ! marks are the price for the one Object.assign, and the cost of ! is real, because it suppresses the only check that a field is ever set, so a field nothing assigns is typed number and holds undefined with no complaint from anyone. Use it where a helper or a bulk assignment genuinely does the work, not to quiet a message.
a private constructor moves the asynchrony into a factory
A constructor cannot be asynchronous, because it must return an object rather than a promise. The refusals first, in a scratch file programs/private-constructor.ts:
class Config {
private constructor(readonly values: Record<string, string>) {}
static create(): Config {
return new Config({});
}
}
export const direct = new Config({});
export class Extended extends Config {}
Check programs/private-constructor.ts
TS2673 [ERROR]: Constructor of class 'Config' is private and only accessible within the class declaration.
export const direct = new Config({});
~~~~~~~~~~~~~~
at file:///programs/private-constructor.ts:9:23
TS2675 [ERROR]: Cannot extend a class 'Config'. Class constructor is marked as private.
export class Extended extends Config {}
~~~~~~
at file:///programs/private-constructor.ts:11:31
error: Type checking failed.
Constructing from outside and extending are both refused, and static create inside the class is fine, which is the whole reason the combination works; use protected instead if subclasses are meant to exist. Delete the scratch file and put the pattern to its best use:
Deno.test("a private constructor moves the asynchrony into a factory", async () => {
class Config {
private constructor(readonly values: Record<string, string>) {}
static async load(): Promise<Config> {
const values = await Promise.resolve({ retries: "3" });
return new Config(values);
}
get retries(): number {
return Number(this.values.retries);
}
}
const config = await Config.load();
assertStrictEquals(config.retries, 3);
const Forced = Config as unknown as new (
values: Record<string, string>,
) => Config;
assertStrictEquals(new Forced({ retries: "9" }).retries, 9);
});
a private constructor moves the asynchrony into a factory ... ok (48µs)
The awaiting happens in load, and every instance is fully built before anyone can hold one. And, one more time, the cast gets through, because there is no private constructor in the emitted class.
a field over an accessor is a trap, and the checker catches it
Add Volume at module scope, a class whose whole purpose is the cap in its setter:
class Volume {
#decibels = 0;
get level(): number {
return this.#decibels;
}
set level(value: number) {
this.#decibels = Math.min(value, 11);
}
}
Write level = 3 in a subclass, in a scratch file programs/accessor-field.ts:
export class Volume {
#decibels = 0;
get level(): number {
return this.#decibels;
}
set level(value: number) {
this.#decibels = Math.min(value, 11);
}
}
export class Loud extends Volume {
level = 3;
}
Check programs/accessor-field.ts
TS2610 [ERROR]: 'level' is defined as an accessor in class 'Volume', but is overridden here in 'Loud' as an instance property.
level = 3;
~~~~~
at file:///programs/accessor-field.ts:14:3
TS4114 [ERROR]: This member must have an 'override' modifier because it overrides a member in the base class 'Volume'.
level = 3;
~~~~~
at file:///programs/accessor-field.ts:14:3
error: Type checking failed.
Two errors on one line, and the first is doing real work. Delete the scratch file, shield the field into the test, and predict whether the base class's cap still holds:
Deno.test("a field over an accessor is a trap, and the checker catches it", () => {
const capped = new Volume();
capped.level = 99;
assertStrictEquals(capped.level, 11);
class Silent extends Volume {
// @ts-expect-error: 'level' is defined as an accessor in class 'Volume', but is overridden here in 'Silent' as an instance property.
level = 3;
}
const silent = new Silent();
silent.level = 99;
assertStrictEquals(silent.level, 11);
});
Check programs/typing-classes.test.ts
running 8 tests from ./programs/typing-classes.test.ts
...
a field over an accessor is a trap, and the checker catches it ... FAILED (8ms)
ERRORS
a field over an accessor is a trap, and the checker catches it => ./programs/typing-classes.test.ts:204:6
error: AssertionError: Values are not strictly equal.
[Diff] Actual / Expected
- 99
+ 11
FAILURES
a field over an accessor is a trap, and the checker catches it => ./programs/typing-classes.test.ts:204:6
FAILED | 7 passed | 1 failed (10ms)
error: Test failed
The cap is gone entirely. The subclass's field did not go through the setter: it defined an own data property on the instance, shadowing the accessor pair on the prototype, so the cap the base class exists to enforce is lost and every later write misses it too. That is ES2022 class field semantics, a field initializer performs a define rather than an assignment, which is correct behaviour and almost never what somebody writing level = 3 in a subclass intends. The fix is to override the accessor with an accessor, where super.level = value reaches the base setter. Correct the prediction and pin both halves:
Deno.test("a field over an accessor is a trap, and the checker catches it", () => {
const capped = new Volume();
capped.level = 99;
assertStrictEquals(capped.level, 11);
class Silent extends Volume {
// @ts-expect-error: 'level' is defined as an accessor in class 'Volume', but is overridden here in 'Silent' as an instance property.
level = 3;
}
const silent = new Silent();
assert(Object.hasOwn(silent, "level"));
assertStrictEquals(silent.level, 3);
silent.level = 99;
assertStrictEquals(silent.level, 99);
class Loud extends Volume {
override get level(): number {
return super.level;
}
override set level(value: number) {
super.level = value * 2;
}
}
const loud = new Loud();
loud.level = 3;
assertStrictEquals(loud.level, 6);
assertFalse(Object.hasOwn(loud, "level"));
});
a field over an accessor is a trap, and the checker catches it ... ok (63µs)
One rough edge worth knowing: declare level: number emits nothing, so it fixes the run-time behaviour, and TypeScript reports TS2610 anyway, while declare override is refused outright with TS1243. There is no spelling that redeclares the member as a field and satisfies the checker, so take the hint and use an accessor, or do not redeclare it at all, since the type is inherited either way.
override the pair, not half of it
A getter alone shadows the whole accessor pair. Shield the write and predict whether it lands, the way every shielded write on the read-only page did:
Deno.test("override the pair, not half of it", () => {
class ReadOnlyVolume extends Volume {
override get level(): number {
return 42;
}
}
const fixed = new ReadOnlyVolume();
assertStrictEquals(fixed.level, 42);
// @ts-expect-error: Cannot assign to 'level' because it is a read-only property.
fixed.level = 3;
assertStrictEquals(fixed.level, 3);
});
Check programs/typing-classes.test.ts
running 9 tests from ./programs/typing-classes.test.ts
...
override the pair, not half of it ... FAILED (215µs)
ERRORS
override the pair, not half of it => ./programs/typing-classes.test.ts:239:6
error: TypeError: Cannot set property level of #<ReadOnlyVolume> which has only a getter
fixed.level = 3;
^
FAILURES
override the pair, not half of it => ./programs/typing-classes.test.ts:239:6
FAILED | 8 passed | 1 failed (2ms)
error: Test failed
It threw. The setter is gone along with the rest of the pair, so the property is read-only, TS2540 says so at check time, and the write throws at run time, which makes this one of the few erased-modifier stories in this entry with a run-time enforcement behind it. The enforcement is JavaScript's, not TypeScript's. Correct the prediction:
Deno.test("override the pair, not half of it", () => {
class ReadOnlyVolume extends Volume {
override get level(): number {
return 42;
}
}
const fixed = new ReadOnlyVolume();
assertStrictEquals(fixed.level, 42);
assertThrows(
() => {
// @ts-expect-error: Cannot assign to 'level' because it is a read-only property.
fixed.level = 3;
},
TypeError,
"Cannot set property level of #<ReadOnlyVolume> which has only a getter",
);
});
override the pair, not half of it ... ok (73µs)
a getter and a setter may have different types
Deno.test("a getter and a setter may have different types", () => {
class Temperature {
#celsius = 0;
get celsius(): number {
return this.#celsius;
}
set celsius(value: number | string) {
this.#celsius = typeof value === "string" ? Number(value) : value;
}
}
const temperature = new Temperature();
temperature.celsius = "21";
assertStrictEquals(temperature.celsius, 21);
temperature.celsius = 3;
assertStrictEquals(temperature.celsius, 3);
});
a getter and a setter may have different types ... ok (36µs)
The setter accepts more than the getter returns, which is the honest shape for a property that normalises its input: reading always gives a number, and writing accepts either. Worth knowing because the symmetric version forces every caller to convert first, which is exactly the work the setter exists to do.
the two features one flag each will take away
Parameter properties and inferred member types both work in Deno, and each is banned by a compiler option you may well want on. Both are capturable with a scratch config. Write programs/erasable.json and programs/flagged.ts:
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
export class Discounted {
constructor(private percent: number) {}
}
Run deno check --config programs/erasable.json programs/flagged.ts:
Check programs/flagged.ts
TS1294 [ERROR]: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
constructor(private percent: number) {}
~~~~~~~~~~~~~~~~~~~~~~~
at file:///programs/flagged.ts:2:15
error: Type checking failed.
erasableSyntaxOnly restricts a file to syntax that can be removed without rewriting anything, and a parameter property fails it because private percent: number in a parameter list emits an assignment, as does enum. The second flag wants annotations where inference would have managed. Write programs/isolated.json and programs/inferred.ts:
{
"compilerOptions": {
"isolatedDeclarations": true
}
}
export class Product {
sku;
constructor(sku: string) {
this.sku = sku;
}
}
Run deno check --config programs/isolated.json programs/inferred.ts:
Check programs/inferred.ts
TS9012 [ERROR]: Property must have an explicit type annotation with --isolatedDeclarations.
sku;
~~~
at file:///programs/inferred.ts:2:3
TS9029 [ERROR]: Add a type annotation to the property sku.
sku;
~~~
at file:///programs/inferred.ts:2:3
error: Type checking failed.
isolatedDeclarations requires that a declaration file can be produced from each file alone, without inference across files, and a field whose type comes from the constructor's assignment fails it, with the hint naming the fix. Neither flag is on by default in Deno. Both are worth turning on in a new project, and that decision is worth making early, because parameter properties are pleasant enough that a codebase fills with them. Delete the four scratch files.
a class instance does not survive a copy
Deno.test("a class instance does not survive a copy", () => {
class Cloneable {
#hidden = "secret";
constructor(public x: number, public y: number) {}
get hidden(): string {
return this.#hidden;
}
}
const original = new Cloneable(1, 2);
assertStrictEquals(original.hidden, "secret");
const cloned = structuredClone(original);
assertFalse(cloned instanceof Cloneable);
assertEquals(Object.keys(cloned), ["x", "y"]);
assertStrictEquals(JSON.stringify(cloned), '{"x":1,"y":2}');
});
a class instance does not survive a copy ... ok (142µs)
structuredClone copies the data and forgets the class: a plain object with the two public fields, no prototype from Cloneable, no accessor, and no #hidden. JSON.stringify loses the same things, which the json page covers, and the designing error types page already worked through what this does to an Error subclass. This is the strongest practical argument for a plain object type: data that crosses a boundary, a worker, a cache, a message, or a JSON file arrives as data, and if your type was a class then something has to rebuild it. A type and some functions have nothing to rebuild.
the same tree, twice, and the trade is which change is cheap
Deno.test("the same tree, twice, and the trade is which change is cheap", () => {
abstract class Expression {
abstract evaluate(): number;
}
class Literal extends Expression {
constructor(private value: number) {
super();
}
override evaluate(): number {
return this.value;
}
}
class Sum extends Expression {
constructor(private left: Expression, private right: Expression) {
super();
}
override evaluate(): number {
return this.left.evaluate() + this.right.evaluate();
}
}
type Node =
| { kind: "literal"; value: number }
| { kind: "sum"; left: Node; right: Node };
function evaluate(node: Node): number {
switch (node.kind) {
case "literal":
return node.value;
case "sum":
return evaluate(node.left) + evaluate(node.right);
}
}
const asClasses = new Sum(
new Literal(2),
new Sum(new Literal(1), new Literal(3)),
);
const asData: Node = {
kind: "sum",
left: { kind: "literal", value: 2 },
right: {
kind: "sum",
left: { kind: "literal", value: 1 },
right: { kind: "literal", value: 3 },
},
};
assertStrictEquals(asClasses.evaluate(), 6);
assertStrictEquals(evaluate(asData), 6);
});
the same tree, twice, and the trade is which change is cheap ... ok (57µs)
Two designs for the same thing, both correct, and they differ in which change is free. Add a kind of node: with classes, write one more subclass and touch nothing else; with the union, every function that switches on kind stops being exhaustive, and each one has to be edited. Add an operation: with the union, write one more function and touch nothing else; with classes, every class needs a new method. So the question is not which is better but which axis your code grows along, and a syntax tree gains operations far more often than it gains node kinds, which is why compilers written in this style tend to be unions and functions. The unions and narrowing page has the narrowing side, and never in a default branch is how you make stops-being-exhaustive an error rather than a silent gap.
The whole entry
Run the whole reference suite:
Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
a class declares a type and a factory ... ok (176µs)
parameter properties declare and assign at once ... ok (175µs)
an abstract class is a template with a hole in it ... ok (41µs)
abstractness is a compile-time fact and nothing else ... ok (285µs)
implements checks the class and does not change it ... ok (30µs)
the definite assignment assertion is a promise you make ... ok (39µs)
a private constructor moves the asynchrony into a factory ... ok (38µs)
a field over an accessor is a trap, and the checker catches it ... ok (68µs)
override the pair, not half of it ... ok (57µs)
a getter and a setter may have different types ... ok (36µs)
a class instance does not survive a copy ... ok (155µs)
the same tree, twice, and the trade is which change is cheap ... ok (72µs)
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...
ok | 682 passed | 0 failed (1s)
Twelve tests, and the practice is short. Annotate what a class declares, even where inference would manage, because a class is a published type and inference makes its API accidental. Use implements on a props interface when a constructor assigns in bulk, the only thing keeping the class and the props in step, at the cost of one word. Reach for abstract when a base class has real shared behaviour and one gap, a discriminated union when the branches carry data and no behaviour, and a plain object type and functions when the thing is data that travels. Never write a field where the base class has an accessor, because the field wins and the accessor's work is silently lost. Set erasableSyntaxOnly in a new project before parameter properties spread through it, and isolatedDeclarations too if you publish types. And expect none of it at run time: private, protected, abstract, implements, and override are all gone, and a cast reaches straight through every one of them. When a boundary must actually hold, that is # for state, a validating function for input, and a test for the rest.