Sergei Basharov
Sergei Basharov

Reputation: 53850

How to type an array with classes in TypeScript?

I have an app that initializes by running its method .init(params) like this:

app.init([TopBar, StatusBar, MainArea]);

Where TopBar, StatusBar and MainArea are classes, not instances of classes. Each of these classes implements the same interface IComponent.

I want to instantiate objects from the passed classes in the .init(params) method, like this:

init(params: IComponent[]): void {
    params.map(function (component) {
        let comp = new component();
        this.components[comp.constructor.name] = comp;
    }, this);

The issue is that as these are not instance, TypeScript doesn't know their types and throws an error:

error TS2345: Argument of type '(typeof TopBar | typeof StatusBar | typeof MainArea)[]' is not assignable to parameter of type 'IComponent[]'.

How do I fix the code so that I could pass an array of classes that implement some interface to a method?

Upvotes: 14

Views: 37353

Answers (6)

codeBelt
codeBelt

Reputation: 1805

I found two different ways you can create types for this situation:

// Interface style:
export default interface IConstructor<T> extends Function {
  new (...args: any[]): T;
}

// Union Type style:
export type ConstructorUnion<T> = new(...args : any[]) => T;

So this is how it would look with the IConstructor type:

interface IComponent { }
class TopBar implements IComponent { }
class StatusBar implements IComponent { }
class MainArea { }

class App {
  public components: { [key: string]: IComponent } = {};

  public init(params: IConstructor<IComponent>[]): void {
    params.forEach((Component: IConstructor<IComponent>) => {
      const comp: IComponent = new Component();

      this.components[comp.constructor.name] = comp;
    });
  }
}

const app = new App();

app.init([TopBar, StatusBar, MainArea]);

console.clear();
console.log(app);

Here is the code: https://stackblitz.com/edit/how-to-type-an-array-with-classes-in-typescript?file=index.ts

Upvotes: 0

erikkallen
erikkallen

Reputation: 34401

Even though this is an old question: this is how you do it:

interface IComponent { something(): void; }
class TopBar implements IComponent { something() { console.log('in TopBar'); }}
class StatusBar implements IComponent { something() { console.log('in StatusBar'); }}
class MainArea implements IComponent { something() { console.log('in MainArea'); }}

interface ComponentClass {
    new(): IComponent;
}

const components: { [name: string]: IComponent } = {};

function init(params: ComponentClass[]) {
    params.map((component) => {
        let comp = new component();
        components[component.name] = comp;
    });
}

init([TopBar, StatusBar, MainArea]);

for (const c in components) {
    console.log('Component: ' + c);
    components[c].something();
}

Upvotes: 1

Wade Kalllhoff
Wade Kalllhoff

Reputation: 151

Typescript supports Class Type Generics (TypeScript Docs). Their example is:

function create<T>(c: {new(): T; }): T {
    return new c();
}

Which says "Pass into my create method a class that when constructed will return the type T that I want". This signature will prevent you from trying to pass in any class type that isn't of type T.

This is close to what we want, we just need to adjust for it being an array of items and items of your IComponent.

public init(components: {new(): IComponent;}[]): void {
    // at this point our `components` variable is a collection of
    // classes that implement IComponent

    // for example, we can just new up the first one;
    var firstComponent = new components[0]();
}, this);

With the method signature, we can now use it like

app.init([TopBar, StatusBar, MainArea]);

Where we pass in the array of types that implement IComponent

Upvotes: 7

Radim K&#246;hler
Radim K&#246;hler

Reputation: 123861

There is a working typescript playground (run it to get alert with result)

what we need is to create a custom type InterfaceComponent. That will be expected as an array of the init() method

interface IComponent { }
class TopBar    implements IComponent { }
class StatusBar implements IComponent { }
class MainArea  implements IComponent { }

// this is a type we want to be passed into INIT as an array
type InterfaceComponent = (typeof TopBar | typeof StatusBar | typeof MainArea);

class MyClass {

  components: {[key:string] : IComponent } = {};

  init(params: (InterfaceComponent)[]): void {
    params.map((component) => {
        let comp = new component();
        this.components[comp.constructor["name"]] = comp;
    }, this);
  }
}

let x = new MyClass();
x.init([TopBar, StatusBar, MainArea])

alert(JSON.stringify(x.components))

Check it here

Upvotes: 6

Brian Herbert
Brian Herbert

Reputation: 1191

Perhaps you could specify the type of comp as InterfaceComponent.

var comp: InterfaceComponent = new component();
this.components[comp.constructor.name] = comp;

Upvotes: 0

davestevens
davestevens

Reputation: 2323

Use a factory method instead. The declaration is a bit clumsy but the idea works:

interface InterfaceComponent {
    name: string;
}

class TopBar implements InterfaceComponent {
    name: string;
}

class StatusBar implements InterfaceComponent {
    name: string;
}

class MainArea implements InterfaceComponent {
    name: string;
}

interface InterfaceComponentFactory {
    create: () => InterfaceComponent;
}

function init(params: InterfaceComponentFactory[]): void {
    params.map(function (component) {
        let comp = component.create();
        this.components[comp.name] = comp;
    }, this);
}

init([{ create: () => new TopBar() }, { create: () => new StatusBar() }, { create: () => new MainArea() }]);

Upvotes: 0

Related Questions