0

Say I have a class Test with a static method .plugin which takes a function. That function can run arbitrary code and extend Tests API.

const MyTest = Test.plugin(fooPlugin)
const test = new Test()
const myTest = new MyTest()
test.foo // does not exist
myTest.foo // exists

I've made a TypeScript Playground that I hope is close to working

When I add myTest.foo to the end of the example, .foo is typed as any. I would expect that the <typeof plugin> would return the type of the plugin function that I pass, not the generic specification?

If I replace <typeof plugin> with <typeof TestPlugin> then it works as expected.

Is there anything I can do to make this work, without changing the way the Plugin Architecture currently works?

If I slightly change the code (Playground link), myTest.foo gets typed correctly, but there are two TypeScript errors.

Community
  • 1
  • 1
Gregor
  • 2,325
  • 17
  • 27

1 Answers1

1

Your modified approach is almost correct, just that T = Plugin is a default value of Plugin for the type parameter T, but T can be any other type not necessarily a subtype of Plugin. You want to say T extends Plugin which means T must be a subtype of Plugin.

Also you don't need the index signature in Test (at least not as far as the plugin architecture is concerned). This will make missing members an error as well (the index signature would hide those):

type ApiExtension = { [key: string]: any }
export type Plugin = (instance: Test) => ApiExtension;
type Constructor<T> = new (...args: any[]) => T;

class Test {
  static plugins: Plugin[] = [];
  static plugin<T extends Plugin>(plugin: T) {
    const currentPlugins = this.plugins;

    class NewTest extends this {
      static plugins = currentPlugins.concat(plugin);
    }

    type Extension = ReturnType<T>
    return NewTest as typeof NewTest & Constructor<Extension>;
  }

  constructor() {
    // apply plugins
    // https://stackoverflow.com/a/16345172
    const classConstructor = this.constructor as typeof Test;
    classConstructor.plugins.forEach(plugin => {
      Object.assign(this, plugin(this))
    });
  }
}

// Question: how to make Typescript understand that MyTest instances have a .foo() method now?
type TestPluginExtension = {
  foo(): 'bar'
}

const TestPlugin = (test: Test): TestPluginExtension => {
  console.log('plugin evalutes')

  return {
    foo: () => 'bar'
  }
}
const MyTest = Test.plugin(TestPlugin)
const myTest = new MyTest()
myTest.foo()
myTest.fooo() //err

Play

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • 1
    That all makes sense. Thanks so much Titian! – Gregor Oct 29 '19 at 16:36
  • I've created a follow up question to allow the static `.plugin()` method to also accept an array of plugins: https://stackoverflow.com/questions/58612535/intersection-types-from-variable-array-of-types I tried to figure out if what I want to do is possible to type at all ... but no luck. Got any idea? – Gregor Oct 29 '19 at 17:23