Plugin architecture example with full TypeScript support
The goal of this repository is to provide a template for a simple plugin Architecture which allows plugin authors to extend the base API as well as extend its constructor options. A custom class can be composed of the core Base class, a set of plugins and default options and distributed as new package, with full TypeScript for added APIs and constructor options.
Try it in TypeScript's playground editor
// import the Base class
import { Base } from "javascript-plugin-architecture-with-typescript-definitions";
// import a set of plugins
import { myFooPlugin } from "@example/my-foo-plugin";
import { myBarPlugin } from "./my-bar-plugin";
export const MyBase = Base.withPlugins([myFooPlugin, myBarPlugin]).withDefaults(
{
foo: "bar",
},
);
When importing MyBase
and instantiating it, the MyBase
constructor has type support for the new optional foo
option as well as the .foo()
and .bar()
methods addded by the respective plugins.
import { MyBase } from "@example/my-base";
const myBase = new MyBase({
// has full TypeScript intellisense
foo: "bar",
});
myBase.foo(); // has full TypeScript intellisense
myBase.bar(); // has full TypeScript intellisense
import { Base } from "javascript-plugin-architecture-with-typescript-definitions";
declare module "javascript-plugin-architecture-with-typescript-definitions" {
namespace Base {
interface Options {
foo?: string;
}
}
}
export function myFooPlugin(base: Base, options: Base.options) {
return {
foo() => options.foo || "bar",
}
}
Returns a new class with .plugins
added to parent classes .plugins
array. All plugins will be applied to instances.
Returns a new class with .defaults
merged with the parent classes .defaults
object. The defaults are applied to the options passed to the constructor when instantiated.
Base.plugins
is an empty array by default. It is extended on derived classes using .withPlugins(plugins)
.
Base.defaults
is an empty object by default. It is extended on derived classes using .withDefaults(plugins)
.
The constructor accepts one argument which is optional by default
new Base(options);
If the Base.Options
interface has been extended with required keys, then the options
argument becomes required, and all required Base.Options
keys must be set.
The .options
key is set on all instances. It's merged from from the constructor's .defaults
object and the options passed to the constructor
const BaseWithOptions = Base.withDefaults({ foo: "bar" });
const instance = new BaseWithOptions();
instance.options; // {foo: 'bar'}
Note that in for TypeScript to recognize the new option, you have to extend the Base.Option
intererface.
Instance properties and methods can be added using plugins. Example:
function myPlugin(base: Base, options: Base.options) {
return {
myMethod() {
/* do something here */
},
myProperty: "", // set to something useful
};
}
const MyBase = Base.plugins([myPlugin]);
const myBase = new MyBase();
// this method and property is now set
myBase.myMethod();
myBase.myProperty;
If you write your d.ts
files by hand instead of generating them from TypeScript source code, you can use the ExtendBaseWith
Generic to create a class with custom defaults and plugins. It can even inherit from another customized class.
import {
Base,
ExtendBaseWith,
} from "javascript-plugin-architecture-with-typescript-definitions";
import { myPlugin } from "./my-plugin.js";
export const MyBase: ExtendBaseWith<
Base,
{
defaults: {
myPluginOption: string;
};
plugins: [typeof myPlugin];
}
>;
// support import to be used as a class instance type
export type MyBase = typeof MyBase;
The last line is important in order to make MyBase
behave like a class type, making the following code possible:
import { MyBase } from "./index.js";
export async function testInstanceType(client: MyBase) {
// types set correctly on `client`
client.myPlugin({ myPluginOption: "foo" });
}
TypeScript will not complain when chaining .withDefaults()
calls endlessly: the static .defaults
property will be set correctly. However, when instantiating from a class with 4+ chained .withDefaults()
calls, then only the defaults from the first 3 calls are supported. See #57 for details.
This plugin architecture was extracted from @octokit/core
. The implementation was made possible by help from @karol-majewski, @dragomirtitian, StackOverflow user "hackape", and @JoshuaKGoldberg.