In the process of implementing a basic graph system with nodes that point to other nodes (node → node) using an identifier code, we got to this simple but acceptable solution:
interface GraphNode {
code: number;
next: number | null;
}
class SeaPainting implements GraphNode {
code = 1;
next = 2; // Does it exist? Which GraphNode does it point to? ...
}
class LavaPainting implements GraphNode {
code = 2;
next = null;
}
function getCodes(nodes: GraphNode[]) {
return nodes.map((node) => node.code);
}
The inconvenient is that we must trust the values we pass to next
are correct—but that could not be possible. Ideally, we may provide a static member to read it like SeaPainting.code
:
interface GraphNode {
next: number | null;
}
class SeaPainting implements GraphNode {
static code = 1;
next = LavaPainting.code; // More intelligible and type-safe
}
class LavaPainting implements GraphNode {
static code = 2;
next = null;
}
To do this, we should change the way we access the static member from an instance object:
function getCodes(nodes: GraphNode[]) {
- return nodes.map((node) => node.code);
^^^^ Property 'code' does not exist on type `GraphNode`
+ return nodes.map((node) => Object.getPrototypeOf(node).constructor.code);
}
But, we cannot straightforwardly define a member of an interface
as static
:
interface GraphNode {
static code: number;
//^^^^^^ 'static' modifier cannot appear on a type member.
next: number | null;
}
A solution could be offering both instance and static members:
interface GraphNode {
code: number;
next: number | null;
}
class SeaPainting implements GraphNode {
static code = 1;
code = SeaPainting.code;
next = LavaPainting.code;
}
class LavaPainting implements GraphNode {
static code = 2;
code = LavaPainting.code;
next = null;
}
function getCodes(nodes: GraphNode[]) {
return nodes.map((node) => node.code);
}
Another solution could be extracting hardcoded values in constants, but here we lose class encapsulation of these values:
const SEA_PAINTING_CODE = 1;
const LAVA_PAINTING_CODE = 2;
interface GraphNode {
code: number;
next: number | null;
}
class SeaPainting implements GraphNode {
code = SEA_PAINTING_CODE;
next = LAVA_PAINTING_CODE;
}
class LavaPainting implements GraphNode {
code = LAVA_PAINTING_CODE;
next = null;
}
function getCodes(nodes: GraphNode[]) {
return nodes.map((node) => node.code);
}
The closest type-safe solution we can get, but it adds verbosity and type complexity when declaring classes:
interface GraphNodeConstructor {
new (): GraphNode;
code: number;
}
interface GraphNode {
next: number | null;
}
const SeaPainting: GraphNodeConstructor = class implements GraphNode {
static code = 1;
next = LavaPainting.code;
};
const LavaPainting: GraphNodeConstructor = class implements GraphNode {
static code = 2;
next = null;
};
function logCodes(nodes: GraphNode[]) {
return nodes.map((node) => Object.getPrototypeOf(node).constructor.code);
}