I'm not sure if I understood you correctly, but I think there is some misconception about how you described the plugin architecture. You shouldn't have to extend/modify/override the plugin's code to use it (e.g., by using namespace & declaration merging because it violates the open-closed principle). To make plugins closed for modification, I would introduce a shared interface that all the plugin packages will use. e.g.
// package: purchases
export interface CarPurchase {
buy():void
}
The package that provides details for purchasing Toyota cars could look like this.
// package: toyota
import { CarPurchase } from "purchases"
export type ToyotaYarisPurchaseOptions {
engine?: "petrol" | "diesel" | "hybrid";
}
export class ToyotaYarisPurchase implements CarPurchase {
constructor(options: ToyotaYarisPurchaseOptions){}
buy():void {
//implementations details goes here
}
}
export type ToyotaCamryPurchaseOptions {
engine?: "petrol" | "diesel" | "hybrid";
}
export class ToyotaCamryPurchase implements CarPurchase {
constructor(options: ToyotaCamryPurchaseOptions){}
buy():void {
//implementations details goes here
}
}
Then, in your main app, I could implement the Dealer class that runs purchases.
import { CarPurchase } from "purchases"
import { ToyotaYarisPurchase } from "toyota"
class Dealer {
makeTransaction(purchase: Purchase):void {
// additional behavior for a dealer - e.g. logging all purchases
purchase.buy();
}
}
const dealer = new Dealer();
const toyotaPurchase = new ToyotaYarisPurchase({engine: 'diesel'})
dealer.makeTransaction(toyotaPurchase);
At this point you could also create a factory for purchases in order to hide instantiation details of a purchase:
// purchasesFactory.ts
import { ToyotaYarisPurchaseOptions } from "toyota"
import { CarPurchase } from "purchases"
export class PurchasesFactory {
create(car: 'yaris', options: ToyotaYarisPurchaseOptions): ToyotaYarisPurchase
create(car: 'camry', options: ToyotaCamryPurchaseOptions):ToyotaCamryPurchase
create(car: 'yaris' | 'camry', options: ToyotaYarisPurchaseOptions | ToyotaCamryPurchaseOptions): CarPurchase {
switch(car) {
case 'camry':
return new ToyotaCamryPurchase(options);
// other cases
}
}
}
so your final app code looks like
import { CarPurchase } from "purchases"
import { ToyotaYarisPurchase } from "toyota"
import { PurchasesFactory } from './purchasesFactory';
class Dealer {
makeTransaction(purchase: Purchase):void {
// additional behavior for a dealer - e.g. logging all purchases
purchase.buy();
}
}
const dealer = new Dealer();
const purchases = new PurchasesFactory();
const toyotaYarisPurchase = purchases.create('yaris', { engine: 'diesel' });
dealer.makeTransaction(toyotaYarisPurchase);
The presented example illustrates roughly the usage of a command pattern. It may be suitable for your problem. I used OOP on purpose to clearly present the responsibilities of particular classes, but you could write it more concisely using functions with generic types. (Beware that, injecting additional dependencies to purchase functions and dealer's makeTransaction would require using more advanced concepts).
// shared
export type Purchase<TOptions extends object> = <TOptions>(options:TOptions) => void;
// toyota package
export const buyToyotaYaris: Purchase<ToyotaYarisPurchaseOptions> = options => {
// some logic here
}
export const buyToyotaCamry: Purchase<ToyotaCamryPurchaseOptions> = options => {
// some logic here
}
// main app
import { buyToyotaCamry } from 'toyota';
const makeTransaction = <TPurchaseOptions extends object>(options: TPurchaseOptions, purchase: Purchase<T>) => {
// dealer logic here
purchase(options)
}
makeTransaction({engine: 'diesel'}, buyToyotaCamry);
Dealerinstance?any, there's nothing you can do, really. Whoever wrote that method signature wanted to accept absolutely anything inoptions, and you can't "extend" that to make it reject things, even with declaration merging. I assume theDealerclass should maybe be generic instead, like this approach shows. Would that meet your needs? If so I could write up an answer; if not, what am I missing?Dealerhave something like a plugin registry? How will it select the plugin depending on themodelparameter of thebuyCarmethod? If you could show this implementation, you might get better answers.