How to define a Typescript type that will only accept instances of some user defined class

Viewed 233

I am writing a simple ECS library, and would like to force my users to provide a name of the system they are defining. I want to use it in error / debugging messages. At the same time I would not like to force them to implement every system as a class.

What I would like to achieve is to allow the user to either provide an object literal that has the "name" property, or to provide an instance of a class. In the later case I do not need the name, as the console.log(instance) would be descriptive enough. As in:

class MyStatefulSystem implements System {
    run(...) {...}
}

createEngine().defineSystem(new MyStatefulSystem());
createEngine().defineSystem({name: "simple system", run: (...)=> {}})

How do I define a type that will only match instances of any class? What I have already, with some simplification, is:

export interface System {
  run: (...) => void;
}

interface ExplicitlyNamedSystem extends System {
  name: string;
}

type SystemClass = ?

export type NamedSystem =
  | ExplicitlyNamedSystem
  | SystemClass;
2 Answers

I would make SystemClass and abstract class that defines the name in the constructor.

Module

export interface System {
  run: () => void;
}

interface ExplicitlyNamedSystem extends System {
  name: string;
}

abstract class SystemClass implements ExplicitlyNamedSystem {
  public readonly name: string;
  constructor() {
    this.name = this.constructor.name;
  }

  abstract run: () => void;
}

type NamedSystem = ExplicitlyNamedSystem | SystemClass;

const defineSystem = (s: NamedSystem) => {
  console.log('Going to run NamedSystem', s.name);
  s.run();
}

export {
  defineSystem.
  SystemClass
}

Userland

import { defineSystem, SystemClass } from 'systemstuff';

class UserDefined extends SystemClass {
  public readonly run = () => {
    console.log('inside running!');
  }
}

defineSystem(new UserDefined());
defineSystem({name: 'bob', run: () => console.log('literal')});

TypeScript doesn't distinguish between class instances and plain-old-JavaScript objects. There is no way to create the union you're looking for.

My recommendation would be to require name on both.

interface System {
  readonly name: string;
  run(): void;
}

class System implements System {
  constructor(readonly name: string) { }
  
  run() {}
}

Both methods will create a System object:

const instance = new System('my-system');

const object: System = {
  name: 'my-system',
  run() {}
}
Related