TypeScript problem with requiring constructors and defaults
The Question
I ran into an interesting TypeScript question.
Here’s the situation: we want to allow users to be able to instantiate new objects in our generic class, so we have this type definition for the parameter:
export class Handbag<T> {
constructor(private type: new() => T) {
let handle = new type();
}
}
Then users of our class can pass provide the class to be instantiated in their constructor:
export class MyBag extends Handbag<Leather> {
constructor() {
super(Leather);
}
}
Now for the question: is there a way to make the parameter optional, and use Object
as the default constructor for the instantiation? The code below is what we have in mind, but it doesn’t work:
export class Handbag<T> {
constructor(private type: (new() => T) = Object) {
let handle = new type();
}
}
Making the parameter optional with ?
and using an if/conditional instead of a default in the parameter list works though:
export class Handbag<T> {
constructor(private type?: (new() => T)) {
let handle = new (type || Object)();
}
}
Why doesn’t the first (having a default in the parameters) work?
An Explanation
If we “unpack” the body of the constructor, handle
actually gets inferred a type of T
. That is, if we were to be explicit with our types, the code would read:
export class Handbag<T> {
constructor(private type: (new() => T) = Object) {
// previous:
// let handle = new type();
let handle: T = new type();
}
}
Now we see why you can’t supply Object
as a default - because new Object
doesn’t give you something of the type T
.
Indeed, in the solution we saw working with let handle = new (type || Object)()
, the variable handle
gets inferred a type of Object
!
So, we’d have a couple of options, depending on whether we need handle
to have a type T
.
If so, then mark the parameter as optional by suffixing it with ?
and wrap the construction in a if/conditional:
export class Handbag<T> {
constructor(private type?: new() => T) {
if (type) {
let handle: T = new type();
} else {
//...
}
}
}
If handle
having a type T
is not important, then this would work:
export class Handbag<T> {
constructor(private type?: new() => any = Object) {
let handle = new type(); // inferred as :any
}
}
But I doubt you would want this, because that was the whole point of the generic <T>
(unless there were other constraints on it).
Learnings
I learnt a couple of things while looking into this:
-
#TIL TypeScript gives you the ability to specify something has a constructor, eg.
interface HasConstructor<T> { new (): T; }
-
The interesting thing is, you can’t really define a type, and at the same time, say
implements HasConstructor<...>
, because you run into a kind of a circular situation where you are defining your type (say,X
) and you’d want to use it at the same time (as the generic parameter toimplements HasConstructor<X>
):class Apples // ok, you want to define Apples... implements HasConstructor< // ok, you want to say it has a constructor Apples // 💥 doesn't work - reference-before-defined > { constructor() { console.log("Apples constructed"); } }
-
BUT - you don’t have to say
implements HasConstructor<...>
for TypeScript to recognize thatApples
satifiesHasConstructor<T>
, because of structural typing 1 as opposed to nominal (using names) typing.class Apples { constructor() { console.log("Apples constructed"); } } class WantsConstructor<T> { constructor(canBeConstructed: HasConstructor<T>) { const thing: T = new canBeConstructed(); } } new WantsConstructor(Apples); // compiles OK
While this works, as the author of
Apples
, with the absence ofimplements ...
, you do lose some of documenting that it provides; it is less obvious to subsequent readers ofApples
that you, the author, had intendedApples
to implement a certain interface. -
BUT - how does TypeScript do it, for, say
Object
? How did it declare the type Object, specify that the constructor returned said type? Wouldn’t it run into a similar situation of a circular/reference-before-defined that we mentioned above? I’ll just link to TypeScript’s type definition forObject
and look into this another day. :)