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 thatApplessatifiesHasConstructor<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 OKWhile 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 ofApplesthat you, the author, had intendedApplesto 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 forObjectand look into this another day. :)