In this chapter about TypeScript, we examine types related to classes and their instances.
Consider this class:
class Counter extends Object {createZero() {
static new Counter(0);
return
}: number;
valueconstructor(value: number) {
super();
.value = value;
this
}increment() {
.value++;
this
}
}// Static method
= Counter.createZero();
const myCounter .ok(myCounter instanceof Counter);
assert.equal(myCounter.value, 0);
assert
// Instance method
.increment();
myCounter.equal(myCounter.value, 1); assert
The diagram in fig. 2 shows the runtime structure of class Counter
. There are two prototype chains of objects in this diagram:
Counter
. The prototype object of class Counter
is its superclass, Object
.myCounter
. The chain starts with the instance myCounter
and continues with Counter.prototype
(which holds the prototype methods of class Counter
) and Object.prototype
(which holds the prototype methods of class Object
).In this chapter, we’ll first explore instance objects and then classes as objects.
Interfaces specify services that objects provide. For example:
interface CountingService {: number;
valueincrement(): void;
}
TypeScript’s interfaces work structurally: In order for an object to implement an interface, it only needs to have the right properties with the right types. We can see that in the following example:
: CountingService = new Counter(3); const myCounter2
Structural interfaces are convenient because we can create interfaces even for objects that already exist (i.e., we can introduce them after the fact).
If we know ahead of time that an object must implement a given interface, it often makes sense to check early if it does, in order to avoid surprises later. We can do that for instances of classes via implements
:
class Counter implements CountingService {// ···
; }
Comments:
.increment
) and own properties (such as .value
).Classes themselves are also objects (functions). Therefore, we can use interfaces to specify their properties. The main use case here is describing factories for objects. The next section gives an example.
The following two interfaces can be used for classes that support their instances being converted from and to JSON:
// Converting JSON to instances
interface JsonStatic {fromJson(json: any): JsonInstance;
}
// Converting instances to JSON
interface JsonInstance {toJson(): any;
}
We use these interfaces in the following code:
class Person implements JsonInstance {fromJson(json: any): Person {
static if (typeof json !== 'string') {
new TypeError(json);
throw
}new Person(json);
return
}: string;
nameconstructor(name: string) {
.name = name;
this
}toJson(): any {
.name;
return this
} }
This is how we can check right away if class Person
(as an object) implements the interface JsonStatic
:
// Assign the class to a type-annotated variable
: JsonStatic = Person; const personImplementsJsonStatic
The following way of making this check may seem like a good idea:
: JsonStatic = class implements JsonInstance {
const Person// ···
; }
However, that doesn’t really work:
new
-call Person
because JsonStatic
does not have a construct signature.Person
has static properties beyond .fromJson()
, TypeScript won’t let us access them.Object
and for its instancesIt is instructive to take a look at TypeScript’s built-in types:
On one hand, interface ObjectConstructor
is for class Object
itself:
/**
* Provides functionality common to all JavaScript objects.
*/
declare var Object: ObjectConstructor;
interface ObjectConstructor {new(value?: any): Object;
: any;
(): any): any;
(value
/** A reference to the prototype for a class of objects. */
readonly prototype: Object;
/**
* Returns the prototype of an object.
* @param o The object that references the prototype.
*/
getPrototypeOf(o: any): any;
}
On the other hand, interface Object
is for instances of Object
:
interface Object {/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
: Function;
constructor
/** Returns a string representation of an object. */
toString(): string;
}
The name Object
is used twice, at two different language levels:
Consider the following class:
class Color {: string;
nameconstructor(name: string) {
.name = name;
this
} }
This class definition creates two things.
First, a constructor function named Color
(that can be invoked via new
):
.equal(
assert, 'function') typeof Color
Second, an interface named Color
that matches instances of Color
:
: Color = new Color('green'); const green
Here is proof that Color
really is an interface:
interface RgbColor extends Color {: [number, number, number];
rgbValue }
There is one pitfall, though: Using Color
as a static type is not a very strict check:
class Color {: string;
nameconstructor(name: string) {
.name = name;
this
}
}
class Person {: string;
nameconstructor(name: string) {
.name = name;
this
}
}
: Person = new Person('Jane');
const person: Color = person; // (A) const color
Why doesn’t TypeScript complain in line A? That’s due to structural typing: Instances of Person
and of Color
have the same structure and are therefore statically compatible.
We can make the two groups of objects incompatible by adding private properties:
class Color {: string;
name= true;
private branded constructor(name: string) {
.name = name;
this
}
}
class Person {: string;
name= true;
private branded constructor(name: string) {
.name = name;
this
}
}
: Person = new Person('Jane');
const person
// @ts-expect-error: Type 'Person' is not assignable to type 'Color'.
// Types have separate declarations of a private property
// 'branded'. (2322)
: Color = person; const color
The private properties switch off structural typing in this case.