bastianplsfix

Classes as values

A class is a value. You can put one in a variable, pass it to a function, and store it in a Map, and to do any of that you need a type for it. The type of the class is not the type of its instances: writing Point in a type position means an instance of Point, and the class itself is typeof Point. One declaration produces both, at the two language levels the what a type is page describes, and almost every confusion in this area is those two being mistaken for each other.

The typing classes page is about what goes inside a class body. This entry is about the class as a thing you hand around.

Create programs/classes-as-values.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:

import {
assert,
assertFalse,
assertStrictEquals,
assertThrows,
} from "@std/assert";

Below the import, add the classes and helpers the whole page shares; every piece gets its own step below.

class Point {
static readonly label = "point";

constructor(readonly x: number, readonly y: number) {}

get length(): number {
return Math.hypot(this.x, this.y);
}
}

interface JsonInstance {
toJson(): unknown;
}

interface JsonStatic {
fromJson(json: unknown): JsonInstance;
}

class Person implements JsonInstance {
static fromJson(json: unknown): Person {
if (typeof json !== "string") throw new TypeError("expected a string");
return new Person(json);
}

constructor(readonly name: string) {}

toJson(): unknown {
return this.name;
}
}

// deno-lint-ignore no-explicit-any
type NewableClass<T> = new (...args: any[]) => T;

// deno-lint-ignore no-explicit-any
type Class<T> = abstract new (...args: any[]) => T;

function createInstance<T>(TheClass: NewableClass<T>, ...args: unknown[]): T {
return new TheClass(...args);
}

function isInstance<T>(TheClass: Class<T>, value: unknown): boolean {
// @ts-expect-error: Type 'unknown' is not assignable to type 'T'.
const before: T = value;
assert(before === value);

if (value instanceof TheClass) {
const after: T = value;
return after !== undefined;
}
return false;
}

function cast<T>(TheClass: Class<T>, value: unknown): T {
if (!(value instanceof TheClass)) {
throw new TypeError(`not an instance of ${TheClass.name}: ${value}`);
}
return value;
}

function assertInstance<T>(
TheClass: Class<T>,
value: unknown,
): asserts value is T {
cast(TheClass, value);
}

abstract class Shape {
abstract describe(): string;
}

class Circle extends Shape {
constructor(readonly radius: number) {
super();
}

override describe(): string {
return `area ${(Math.PI * this.radius * this.radius).toFixed(2)}`;
}
}

Follow the page as you add and revise the runnable examples below it.

the class and its instances have different types

Deno.test("the class and its instances have different types", () => {
function createPoint(PointClass: typeof Point, x: number, y: number): Point {
return new PointClass(x, y);
}

const point: Point = new Point(3, 4);
const PointClass: typeof Point = Point;

assertStrictEquals(point.length, 5);
assertStrictEquals(createPoint(PointClass, 6, 8).length, 10);
assertStrictEquals(PointClass.label, "point");
});
Check programs/classes-as-values.test.ts
running 1 test from ./programs/classes-as-values.test.ts
the class and its instances have different types ... ok (202µs)

ok | 1 passed | 0 failed (2ms)

typeof Point is the type of the class: it can be called with new, and it has the static members. Point is the type of an instance: it has x, y, and length, and it has no label, because label belongs to the class and not to the objects the class makes. The typeof here is a type operator rather than the JavaScript operator of the same name, appearing only in type positions and meaning the type of that value.

the instance type is an interface you can extend

Deno.test("the instance type is an interface you can extend", () => {
class Color {
constructor(readonly name: string) {}
}

interface RgbColor extends Color {
rgb: [number, number, number];
}

const red: RgbColor = { name: "red", rgb: [255, 0, 0] };
const fromClass: Color = new Color("green");

assertStrictEquals(red.name, "red");
assertStrictEquals(fromClass.name, "green");
assertFalse(red instanceof Color);
});
the instance type is an interface you can extend ... ok (230µs)

interface RgbColor extends Color compiles, which is the proof that a class declaration really does produce an interface. And red is a plain object literal that satisfies it, with no new anywhere and a failing instanceof to show for it, because object types match by structure and a class-derived type is an object type like any other. That last part is either a feature or a bug depending on your day, and the nominal types and branding page is the entry for when it is a bug.

three ways to type a class-shaped parameter

Deno.test("three ways to type a class-shaped parameter", () => {
function fromConstructorType(
PointClass: new (x: number, y: number) => Point,
x: number,
y: number,
): Point {
return new PointClass(x, y);
}

function fromConstructSignature(
PointClass: { new (x: number, y: number): Point; label: string },
x: number,
y: number,
): string {
return `${PointClass.label} ${new PointClass(x, y).length}`;
}

assertStrictEquals(fromConstructorType(Point, 3, 4).length, 5);
assertStrictEquals(fromConstructSignature(Point, 3, 4), "point 5");
});
three ways to type a class-shaped parameter ... ok (26µs)

In the order they should occur to you. typeof Point when you mean that class: shortest, and it carries every static member along with it. A constructor type literal, new (x: number, y: number) => Point, when you mean any class that makes those instances from those arguments: a function type with new in front, saying nothing about statics. And a construct signature inside an object type when the value has other members you also need: { new (...): Point; label: string } demands a class that constructs and carries a label, which is the fifth kind of member from the object types page, the last one this series had left untaught.

an interface for the static side needs its own check

Deno.test("an interface for the static side needs its own check", () => {
const PersonIsJsonStatic: JsonStatic = Person;

const ada = Person.fromJson("Ada");
assertStrictEquals(ada.name, "Ada");
assertStrictEquals(ada.toJson(), "Ada");
assertStrictEquals(PersonIsJsonStatic.fromJson("Ada").toJson(), "Ada");

class Anonymous implements JsonInstance {
toJson(): unknown {
return null;
}
}

// @ts-expect-error: Property 'fromJson' is missing in type 'typeof Anonymous' but required in type 'JsonStatic'.
const AnonymousIsJsonStatic: JsonStatic = Anonymous;
assertStrictEquals(AnonymousIsJsonStatic.fromJson as unknown, undefined);
});
an interface for the static side needs its own check ... ok (29µs)

implements checks the instance side and nothing else, so an interface that describes a class's static members has no keyword to attach it. Person's implements JsonInstance covers toJson, and the static fromJson is covered by the first line, an annotated variable holding the class, which the checker verifies like any other assignment: one line of emitted JavaScript, and the error arrives at the class rather than at whatever call site tries to use it later. The pair is a natural shape for serialisation, the instance knowing how to write itself and the class knowing how to read one back, neither half expressible in one interface because they live on different objects, which the classes page showed with Reflect.ownKeys. Leave the static out and the assignment is where it fails, and note the type in the shielded message: typeof Anonymous, not Anonymous, the checker talking about the class; the last pin shows the shielded claim is also a runtime lie, since fromJson is simply not there.

the tidier version does not compile

The obvious wish is to fold the static check into the declaration, using a class expression and satisfies so nothing extra is emitted. Try it in a scratch file programs/circular.ts:

interface JsonInstance {
toJson(): unknown;
}

interface JsonStatic {
fromJson(json: unknown): JsonInstance;
}

const Person = class implements JsonInstance {
static fromJson(json: unknown): Person {
if (typeof json !== "string") throw new TypeError("expected a string");
return new Person(json);
}

constructor(readonly name: string) {}

toJson(): unknown {
return this.name;
}
} satisfies JsonStatic;

type Person = typeof Person.prototype;
Check programs/circular.ts
TS7022 [ERROR]: 'Person' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
const Person = class implements JsonInstance {
~~~~~~
at file:///programs/circular.ts:9:7

TS2577 [ERROR]: Return type annotation circularly references itself.
static fromJson(json: unknown): Person {
~~~~~~
at file:///programs/circular.ts:10:35

TS2456 [ERROR]: Type alias 'Person' circularly references itself.
type Person = typeof Person.prototype;
~~~~~~
at file:///programs/circular.ts:22:6

Found 3 errors.

error: Type checking failed.

It fails three times, once per link in the cycle. The type Person is defined in terms of the value Person, and the value mentions the type in its own body, so neither can be worked out first, and renaming the alias does not help, because the cycle is the dependency and not the name. Worth knowing this does not work, because it is the first thing anyone tries: the annotated variable is the answer, not a compromise. Delete the scratch file.

one type for any class

Give the constructor type a type parameter and you have a type for a-class-whose-instances-are-T, the two aliases from the module block:

// deno-lint-ignore no-explicit-any
type NewableClass<T> = new (...args: any[]) => T;

// deno-lint-ignore no-explicit-any
type Class<T> = abstract new (...args: any[]) => T;

Two of them, and the difference is at the end of this entry. This is the only place the page needs a type parameter, so take it as given that T stands for the instance type; a full account of generics is ground this series has not covered yet. The any[] deserves its lint ignore, because a reviewer will ask. The two obvious replacements both fail, verified: new (...args: unknown[]) => T rejects almost every class, since assigning Point to it is TS2322, because a constructor taking unknown arguments cannot stand in for one taking numbers; and new (...args: never[]) => T accepts every class and then refuses every argument, so it works for instanceof and not for construction. The any buys the one thing both alternatives lose: a type that matches any class and can still be called.

Deno.test("new implemented as a function", () => {
const ada = createInstance(Person, "Ada");
const point = createInstance(Point, 3, 4);

assertStrictEquals(ada.name, "Ada");
assertStrictEquals(point.length, 5);
});
new implemented as a function ... ok (28µs)

createInstance(Person, "Ada") returns something typed Person, inferred from the argument. The arguments themselves are unchecked, which is the honest limit of this shape: a variadic factory over any class cannot also verify the constructor's parameters.

instanceof on a class parameter narrows unknown to T

{ x: 3, y: 4 } has everything a Point has except provenance. Predict what isInstance says:

Deno.test("instanceof on a class parameter narrows unknown to T", () => {
assertStrictEquals(isInstance(Point, { x: 3, y: 4 }), true);
});
Check programs/classes-as-values.test.ts
running 6 tests from ./programs/classes-as-values.test.ts
...
instanceof on a class parameter narrows unknown to T ... FAILED (10ms)

ERRORS

instanceof on a class parameter narrows unknown to T => ./programs/classes-as-values.test.ts:172:6
error: AssertionError: Values are not strictly equal.

[Diff] Actual / Expected

- false
+ true

FAILURES

instanceof on a class parameter narrows unknown to T => ./programs/classes-as-values.test.ts:172:6

FAILED | 5 passed | 1 failed (12ms)

error: Test failed

The lookalike fails, because instanceof asks about the prototype chain rather than the shape, the nominal check the branding page builds on. That is exactly what makes the helper trustworthy, and the finding that makes these types worth having sits inside isInstance itself: outside the branch, unknown is not assignable to T and the shielded line says so, while inside it the same value is a T with no cast anywhere, because TypeScript understands that value instanceof TheClass establishes the instance type of whatever class was passed, even though nobody knows which class that is until the call. Correct the prediction, and put the same move to work in cast, the version of as that is true at run time:

Deno.test("instanceof on a class parameter narrows unknown to T", () => {
assert(isInstance(Point, new Point(3, 4)));
assertFalse(isInstance(Point, { x: 3, y: 4 }));

const parsed: unknown = new Person("Ada");

assertStrictEquals(cast(Person, parsed).name, "Ada");
assertThrows(
() => cast(Point, parsed),
TypeError,
"not an instance of Point",
);
});
instanceof on a class parameter narrows unknown to T ... ok (693µs)

Compare parsed as Person, which changes the type and checks nothing. cast costs one instanceof.

the same thing as an assertion function removes an any

Deno.test("the same thing as an assertion function removes an any", () => {
const parsed = JSON.parse("[1, 2]");

assert(parsed.anything?.deeply?.nested === undefined);

assertInstance(Array, parsed);

// @ts-expect-error: Property 'anything' does not exist on type 'unknown[]'.
assert(parsed.anything === undefined);
assertStrictEquals(parsed.length, 2);
});
the same thing as an assertion function removes an any ... ok (33µs)

Before the call, parsed is any and three imaginary properties are legal. After it, parsed is unknown[] and one imaginary property is the shielded error. The assertion function did not just narrow a union; it took a value out of any and put it in the type system, with a run-time check backing the claim. The json page calls JSON.parse the main door through which any enters a typed program, and this closes that door in one line for anything whose shape is a class. The elements are still unknown, which is correct, because knowing it is an array says nothing about what is in it, the good outcome by the any, unknown, never page's standard.

a map whose keys are classes

TypeSafeMap checks its claim twice, once in the types and once in cast. Shield the checker's half away and predict what the runtime half does:

Deno.test("a map whose keys are classes", () => {
class TypeSafeMap {
#data = new Map<unknown, unknown>();

get<T>(key: Class<T>): T {
return cast(key, this.#data.get(key));
}

set<T>(key: Class<T>, value: T): this {
cast(key, value);
this.#data.set(key, value);
return this;
}

has(key: unknown): boolean {
return this.#data.has(key);
}
}

const map = new TypeSafeMap();
map.set(RegExp, /a+b/);
map.set(Date, new Date(0));

assertStrictEquals(map.get(RegExp).source, "a+b");
assertStrictEquals(map.get(Date).getTime(), 0);
assertFalse(map.has(Person));

// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'Date'.
map.set(Date, "not a date");
assertStrictEquals(map.get(Date).getTime(), 0);
});
Check programs/classes-as-values.test.ts
running 8 tests from ./programs/classes-as-values.test.ts
...
a map whose keys are classes ... FAILED (551µs)

ERRORS

a map whose keys are classes => ./programs/classes-as-values.test.ts:200:6
error: TypeError: not an instance of Date: not a date
throw new TypeError(`not an instance of ${TheClass.name}: ${value}`);
^

FAILURES

a map whose keys are classes => ./programs/classes-as-values.test.ts:200:6

FAILED | 7 passed | 1 failed (2ms)

error: Test failed

The shield silenced the checker and the cast inside set threw anyway, which is the whole point of the design: two checks for one claim. On every other page of this series, a shielded write landed; this one refused, because a class is available at run time, so a type-level promise can have a run-time enforcement standing behind it, which is unusual in TypeScript and worth exploiting when you can. The key is a class, so it determines the type of the value on the way in and on the way out, and map.get(RegExp) is typed RegExp with no annotation and no cast. Correct the prediction:

Deno.test("a map whose keys are classes", () => {
class TypeSafeMap {
#data = new Map<unknown, unknown>();

get<T>(key: Class<T>): T {
return cast(key, this.#data.get(key));
}

set<T>(key: Class<T>, value: T): this {
cast(key, value);
this.#data.set(key, value);
return this;
}

has(key: unknown): boolean {
return this.#data.has(key);
}
}

const map = new TypeSafeMap();
map.set(RegExp, /a+b/);
map.set(Date, new Date(0));

assertStrictEquals(map.get(RegExp).source, "a+b");
assertStrictEquals(map.get(Date).getTime(), 0);
assertFalse(map.has(Person));

assertThrows(
() => {
// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'Date'.
map.set(Date, "not a date");
},
TypeError,
"not an instance of Date",
);
});
a map whose keys are classes ... ok (89µs)

only one of the two class types accepts an abstract class

Put an abstract Shape where the constructible type wants a class, in a scratch file programs/class-types.ts:

// deno-lint-ignore no-explicit-any
type NewableClass<T> = new (...args: any[]) => T;

// deno-lint-ignore no-explicit-any
type Class<T> = abstract new (...args: any[]) => T;

abstract class Shape {
abstract describe(): string;
}

class Circle extends Shape {
constructor(readonly radius: number) {
super();
}

override describe(): string {
return `area ${(Math.PI * this.radius * this.radius).toFixed(2)}`;
}
}

export const constructible: Array<NewableClass<Shape>> = [Circle, Shape];

export function createInstance<T>(TheClass: Class<T>, ...args: unknown[]): T {
return new TheClass(...args);
}
Check programs/class-types.ts
TS2322 [ERROR]: Type 'typeof Shape' is not assignable to type 'NewableClass<Shape>'.
Cannot assign an abstract constructor type to a non-abstract constructor type.
export const constructible: Array<NewableClass<Shape>> = [Circle, Shape];
~~~~~
at file:///programs/class-types.ts:21:67

TS2511 [ERROR]: Cannot create an instance of an abstract class.
return new TheClass(...args);
~~~~~~~~~~~~~~~~~~~~~
at file:///programs/class-types.ts:24:10

error: Type checking failed.

Two refusals, one per direction, and both correct rather than annoying. new (...args: any[]) => T promises the value can be constructed, and an abstract class cannot be, so Shape is refused. abstract new accepts both kinds and gives up the one thing the other has, so new TheClass(...) on a Class<T> is refused too, which is why the module block's createInstance takes NewableClass<T>. Delete the scratch file and pin the working split:

Deno.test("only one of the two class types accepts an abstract class", () => {
const constructible: Array<NewableClass<Shape>> = [Circle];
const anyShapeClass: Array<Class<Shape>> = [Circle, Shape];

assertStrictEquals(
createInstance(constructible[0], 1).describe(),
"area 3.14",
);
assert(isInstance(anyShapeClass[1], new Circle(1)));
assertStrictEquals(anyShapeClass.length, 2);
});
only one of the two class types accepts an abstract class ... ok (256µs)

So there are two types and you choose by what you need: Class<T> for instanceof, cast, a registry key, or anything that only reads, and NewableClass<T> when you are going to call new. Naming them apart is worth more than picking one and casting around it, and note that the abstract version still narrows perfectly well, as isInstance taking a Class<Shape> shows.

a registry is the shape that wants all of this

Deno.test("a registry is the shape that wants all of this", () => {
const REGISTRY = new Map<string, NewableClass<JsonInstance>>([
["person", Person],
]);

function reviveFrom(kind: string, json: unknown): JsonInstance {
const TheClass = REGISTRY.get(kind);
if (TheClass === undefined) throw new TypeError(`unknown kind: ${kind}`);
return new TheClass(json);
}

assertStrictEquals(cast(Person, reviveFrom("person", "Ada")).name, "Ada");
assertThrows(() => reviveFrom("ghost", "x"), TypeError, "unknown kind");
});
a registry is the shape that wants all of this ... ok (58µs)

A Map from a string to a class, a lookup, and a new, nine lines in all. The types make the lookup's result constructible and its instances usable, which is the shape that wants everything on this page at once.

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/branding.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes-as-values.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-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 9 tests from ./programs/branding.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 10 tests from ./programs/classes-as-values.test.ts
the class and its instances have different types ... ok (229µs)
the instance type is an interface you can extend ... ok (47µs)
three ways to type a class-shaped parameter ... ok (25µs)
an interface for the static side needs its own check ... ok (38µs)
new implemented as a function ... ok (30µs)
instanceof on a class parameter narrows unknown to T ... ok (331µs)
the same thing as an assertion function removes an any ... ok (44µs)
a map whose keys are classes ... ok (88µs)
only one of the two class types accepts an abstract class ... ok (46µs)
a registry is the shape that wants all of this ... ok (53µs)
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 10 tests from ./programs/typing-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
...
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 | 711 passed | 0 failed (1s)

Ten tests, and the practice is short. Use typeof C when you mean one particular class, and a constructor type when you mean a family of them, because the first is shorter and keeps the statics while the second lets a caller pass a class you have never heard of. Reach for Class<T> when you are writing a registry, a factory, or a cast helper, and not before, since a function that takes one known class should say so. Prefer a checked cast to an assertion, because cast(Person, value) and value as Person produce the same type and only one of them is true. Check the static side with an annotated variable, one line and one emitted statement, with the error landing on the class instead of on a caller. And expect Class<T> to reject your abstract base class from construction, knowing which of the two spellings you meant before you reach for a cast to make the complaint go away.