Frank Weindel
Frank Weindel

Reputation: 1282

Enforce 'typeof BaseClass' when base class is generic

Let's say I have a generic abstract base class and a set of classes that extend the base and provide the generic param.

abstract class BaseClass<T> {
  constructor(protected container: T[]) { }
  static someStaticFunc(): string {
    return 'I\'m the BaseClass';
  }
  getElement(i: number): T {
    return this.container[i]
  }
}

class ConcreteClass1 extends BaseClass<number> {
  static someStaticFunc(): string {
    return 'I\'m ConcreteClass1'
  }

  addNumber(element: number) {
    this.container.push(element);
  }
}

class ConcreteClass2 extends BaseClass<string> {
  static someStaticFunc(): string {
    return 'I\'m ConcreteClass2'
  }

  addStringTwice(element: string) {
    this.container.push(element);
    this.container.push(element);
  }
}

class ConcreteClass3 extends BaseClass<boolean> {
    // Doesn't add anything
}

I define an interface structure where each value must be a typeof BaseClass

interface SetOfBaseClasses {
  Class1: typeof BaseClass,
  Class2: typeof BaseClass,
  Class3: typeof BaseClass
};

I want to create an object of that interface type, and assign the concrete classes to it. And then call each of the static functions on the class and get the expected results at the bottom.

const classes: SetOfBaseClasses = {
  Class1: ConcreteClass1,
  Class2: ConcreteClass2,
  Class3: ConcreteClass3
}

console.log(classes.Class1.someStaticFunc());
console.log(classes.Class2.someStaticFunc());
console.log(classes.Class3.someStaticFunc());

// Expected output:
// I'm ConcreteClass1
// I'm ConcreteClass2
// I'm the BaseClass

However, TypeScript does not see ConcreteClass1, ConcreteClass2 or ConcreteClass3 as compatible with typeof BaseClass. I figure this is because of the generic involved with BaseClass. So I also tried:

interface SetOfBaseClasses {
  Class1: typeof BaseClass<unknown>,
  Class2: typeof BaseClass<unknown>,
  Class3: typeof BaseClass<unknown>
};

and even more precisely:

interface SetOfBaseClasses {
  Class1: typeof BaseClass<number>,
  Class2: typeof BaseClass<string>,
  Class3: typeof BaseClass<boolean>
};

In either case, TypeScript does not seem to allow a generic parameter after typeof Class

I get two errors for each line in the interface that are something like this:

Call signature, which lacks return-type annotation, implicitly has an 'any' return type.(7020)
Type parameter name cannot be 'number'.(2368)

Is there anyway to do this type of thing with TypeScript? Everything seems to be fine with this approach until you involve generics.

Typescript Playground

Upvotes: 1

Views: 956

Answers (1)

jcalz
jcalz

Reputation: 329258

The type typeof BaseClass is more or less equivalent to this:

type TypeofBaseClass =
  (abstract new <T>(container: T[]) => BaseClass<T>) &
  { someStaticFunc(): string };

const testTypeofBaseClass: TypeofBaseClass = BaseClass;

Note that TypeofBaseClass is not itself generic (there is no type parameter on it); rather, it has an abstract construct signature which is generic. This is much like a generic function in that the generic type parameter is specified when you call it, not in the declaration itself. A value of typeof BaseClass (or rather, a concrete generic subclass of it like class ConcreteSubclass<T> extends BaseClass<T>) can construct a BaseClass<T> for any T that the person who writes new ConcreteSubclass wants. It could be a BaseClass<string> or a BaseClass<number> or anything else.

That's not the type you want.


Instead, the type you want here is more like this:

type TypeofConcreteClass<T> =
  (new (container: T[]) => BaseClass<T>) &
  { someStaticFunc(): string; };

This is a concrete constructor so the abstract is removed. But more importantly, the scope of the generic type parameter T has been moved. You want to be able to specify TypeofConcreteClass<number> which can only construct BaseClass<number> instances and not anything else. The person who writes new ConcreteClass1 will never be able to get a BaseClass<string> out.


It is not currently possible to automatically convert typeof BaseClass into TypeofConcreteClass<T> without jumping though ugly hoops. The language does not really have a way of representing the relationship between them programmatically. See "TypeScript how to create a generic type alias for a generic function?" and its answer for more information.

For the above, I'd recommend just using a manually written TypeofConcreteClass<T> definition. Or you could get part of the way there by doing something like

type TypeofConcreteClass<T> =
  Pick<typeof BaseClass, keyof typeof BaseClass> &
  (new (container: T[]) => BaseClass<T>);

which programmatically grabs all the static properties/methods of BaseClass while manually writing out the construct signature.


Let's verify that this works how you want:

interface SetOfBaseClasses {
  Class1: TypeofConcreteClass<number>,
  Class2: TypeofConcreteClass<string>,
  Class3: TypeofConcreteClass<boolean>
};

function main() {
  const classes: SetOfBaseClasses = {
    Class1: ConcreteClass1,
    Class2: ConcreteClass2,
    Class3: ConcreteClass3
  }

  console.log(classes.Class1.someStaticFunc());
  console.log(classes.Class2.someStaticFunc());
  console.log(classes.Class3.someStaticFunc());

  console.log(new classes.Class1([1, 2, 3]).getElement(2).toFixed(3)) // "3.000"
  console.log(new classes.Class2(["a", "b", "c"]).getElement(2).toUpperCase()) // "C"

}

That compiles with no error and works as desired.

Playground link to code

Upvotes: 1

Related Questions