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 to implements 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 that Apples satifies HasConstructor<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 of implements ..., you do lose some of documenting that it provides; it is less obvious to subsequent readers of Apples that you, the author, had intended Apples 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 for Object and look into this another day. :)