Kousha
Kousha

Reputation: 36189

Typescript static method in an abstract class to create an instance of self

Let's say I have

abstract class Foo {

}

class Bar1 extends Foo {
    constructor(someVar) { ... }
}

class Bar2 extends Foo {
    constructor(someVar) { ... }
}

I'd like to be able to create a static method that creates an instance of the final class (all constructors would have the same signature). So I want something like:

abstract class Foo {
    public static someAction(someVar) {
        const self = new this(someVar);
    }
}

But this cannot be done because Foo is abstract. Is this at all possible?

UPDATE

What if these classes have their own templates?

abstract class Foo<M> {

}

class Bar1 extends Foo<SomeModel> {...}

Now I want the someAction to know of the type SomeModel. I tried

public static someAction<A, T extends Foo<A>>(this: new (someVar: any) => T, someVar: any): T {
    const self = new this(someVar);
    return self;
  }

But unless I specifically do Bar1.someAction<SomeModel>("blah") the returned result is not available of the type of the data, i.e. Bar1.someAction("blah") doesn't know the data type

Upvotes: 10

Views: 8567

Answers (3)

giorgiline
giorgiline

Reputation: 1371

It think this problem should be managed with the factory method pattern, and move the static method from the abstract class to the factory class.

So then you will have:

  • a factory class with the static method responsible for creating the specific classes.
  • an abstract class with the shared data, logic and the required abstract components to implement.
  • any extending classes with implementation of the corresponding abstract properties and methods from the parent class.

So starting the same way as you had,

there is an abstract class with the shared data,

abstract class Foo{
  constructor(someVar: any){
    ...
  }
}

and then there are some classes that extend the abstract class,

class Bar1 extends Foo {
  constructor(someVar: any){
    super(someVar)
  }
}

class Bar2 extends Foo {
  constructor(someVar: any){
    super(someVar)
  }
}

to create each of those classes based on parameters, it's easy to have a factory with the creational logic:

class BarFactory {
  public static barCreator(someVar: any): Bar1 | Bar2 {
    if(someVar.x === 'whatever'){
      return new Bar1(someVar)    
    } else {
      return new Bar2(someVar)  
    }
  }
}

At last you just get and use the instance using the factory.

const bar = BarFactory.barCreator(someVar)

All abstract properties and methods are directly available.

But f you want to use specific methods or properties of the instantiated class then you will need to check the instance type.

if (bar instanceof Bar1){
  ...
} else {
  ...
}

Upvotes: 1

Tom Daniel
Tom Daniel

Reputation: 323

After some monkeying around I found a way to address your problem with generics.

The key is to not use a type variable in the extends constraint for the base class but to add the base class with the type variable as an intersection of the return type.

Use any instead of T here static someAction<A, T extends Foo<any>> and to add & Foo<A> on the return type.

abstract class Foo<M> {...}

class Bar1 extends Foo<SomeModel> {...}
public static someAction<A, T extends Foo<any>>(
    this: new (someVar: any) => T, someVar: any
): T & Foo<A> {
    return new this(someVar);
}

I was facing the exact same issue while trying to create a user defined type guard, and the same technique above also solve this problem.

public static isThisType<A, T extends Foo<any>>(
    this: new (...args: any) => T, value: unknown
): value is T & Foo<A> {
    return value instanceof this;
}

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249506

You can add an annotation for the this type of the static method. In this case this will refer to the class, and adding an annotation for the this parameter will make the static method visible only on classes that satisfy the constraint (in this case that a constructor with a single argument exists) and it will also help us extract the actual instance type of the class the method is invoked on :

abstract class Foo {
  public static someAction<T extends Foo>(this: new (someVar: any) => T, someVar: any): T {
    const self = new this(someVar);
    return self;
  }
}

class Bar1 extends Foo {

  constructor(someVar: any) {
    super();
  }
}

class Bar2 extends Foo {
  constructor(someVar: any) {
    super();
  }
}
let bar1 = Bar1.someAction(0) // of type Bar1
let bar2 = Bar2.someAction(0) // of type Bar2

Upvotes: 20

Related Questions