Search code examples
typescriptgenericstype-constraints

How to add "newable" constraint for generic parameter in typescript?


I already know how to add "newable" (i.e. has constructor) constraint for function arguments (like argument for foo function below), but same techique doesn't apply to generic type parameters.

Why is that and how to fix it ?

enter image description here

type NoParameterCtor<T> = { new(): T }

function foo<T>(ctor: NoParameterCtor<T>) { }

interface Bar<T extends NoParameterCtor<T>> { }

class Zoo { }

foo(Zoo) 
// no compiler error

class Zar implements Bar<Zoo> { }
// Type 'Zoo' does not satisfy the constraint 'NoParameterCtor<Zoo>'

Solution

  • As mentioned in the comments, T extends NoParameterCtor<T> is an unusual constraint that means "T is a constructor that makes new instances of itself". Unless you are trying to describe self-replicating constructors, this is not what you mean.

    If you just want T do be "anything newable", then you don't need to care about the instance type. Assuming you're using TS3.0 or later you can use unknown to mean any type, although you can use also any. So perhaps you want Bar to be

    interface Bar<T extends NoParameterCtor<unknown>> { }
    

    The following still won't work though:

    class Zar implements Bar<Zoo> { } // error! 
    // Zoo does not satisfy the constraint NoParameterCtor<unknown>
    

    That's because the type Zoo is not newable; it's the instance type of the Zoo class. I don't know if you're confused about the difference between named values and named types in TypeScript, but you're in good company if so. In short, class Zoo {} introduces a type named Zoo, which is the type of instances of the class, and a value named Zoo, which is the constructor of such instances. And the type of the Zoo value is not the Zoo type. To refer to the type of the Zoo constructor value, you need to use typeof Foo instead:

    class Zar implements Bar<typeof Zoo> { } // okay
    

    Also I assume you've stripped out the contents of Bar, Zar and Zoo because they're not relevant here. But just to be clear, empty interfaces match just about everything because TypeScript uses structural typing. If Bar needs access to the instance type of T, then you can use the built-in library type alias InstanceType<> to get it:

    interface Bar<T extends NoParameterCtor<unknown>> {
       theConstructor: T,
       theInstance: InstanceType<T>
    }
    
    class Zar implements Bar<typeof Zoo> { 
      theConstructor = Zoo; // the constructor
      theInstance = new Zoo(); // an instance
    }
    

    Hope that helps. Good luck!