I am in the process of developing a library that allows for the integration of external implementations, and I am exploring the optimal approach to defining types for these implementations.
Illustration
abstract class Creature {
public abstract makeNoises();
}
class Lion extends Creature {
public makeNoises() {
console.log('roar');
}
}
class Elephant extends Creature {
public makeNoises() {
console.log('trumpet');
}
}
type BuiltInCreatures = 'lion' | 'elephant';
interface CreatureLike {
[name: string]: new () => Creature;
}
default class SafariClient {
public starCreature: Creature;
constructor(someCreature: BuiltInCreatures | CreatureLike) {
if (typeof someCreature === 'string') {
// load `Lion` for 'lion', or `Elephant` for 'elephant'.
// this.starCreature = new Lion() or new Elephant();
} else {
// integrate external creature plugin
// this.starCreature = new [someCreature]();
}
}
public makeNoises() {
this.starCreature.makeNoises();
}
}
I intend to offer predefined classes that can be easily utilized, while also allowing users to introduce their own custom classes. How can I achieve this?
const safari = new SafariClient('lion');
// or
const safari = new SafariClient(new Giraffe()); // Or maybe `new SafariClient(Giraffe)`?
I am specifically interested in finding an elegant solution that provides clear options to users of SafariClient
- the type system should indicate that they can use either a string (BuiltInCreature
) or a custom implementation of Creature
.