Skip to content

Instantly share code, notes, and snippets.

@albertms10
Last active November 5, 2024 22:57
Show Gist options
  • Save albertms10/538df7c487c25d1c9ee64f9b212f9795 to your computer and use it in GitHub Desktop.
Save albertms10/538df7c487c25d1c9ee64f9b212f9795 to your computer and use it in GitHub Desktop.
Graph Nodes in TypeScript

Graph Nodes in TypeScript

Description

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;
}

Solutions

Defining both members

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);
}

Using constants

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);
}

Define constructor interface

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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment