3

I have a singleton class that is an extension of a 20x20 array. (I'm making a word search.) I discovered this delightful syntax for exporting a singleton class's instance maintaining both the name of the instance and that of the class:

const Grid = new class Grid extends Array<string[]> {
  constructor(private readonly size = 20) {
    super(size);
    // ...
  }

  // ...
};

export default Grid;

Huzzah, that works great. Now to test. To make things manageable, I want to create a new grid with a much smaller size (maybe 8x8). In JavaScript, retrieving the constructor is easy.

import Grid from './grid';

const GridConstructor = Grid.constructor;

it('does a thing', () => {
  const testGrid = new GridConstructor(8);
});

Since I know what the argument expects, yes, I can just do that and declare GridConstructor as

const GridConstructor: new(size: number) => typeof Grid = Grid.constructor;

That, of course, is the reasonable solution. I am not a reasonable person though, and must type ALL THE THINGS using the TypeScript typing engine. (Okay, so I really just like learning. Who knows when this may be useful?) TypeScript thinks that Grid.constructor is of type Function. As far as I can tell, it doesn't have any information on what that function's shape is.

Is it possible to infer that shape?

The best I've come up with is:

type Constructor<T extends { constructor: new(...args: any[]) => any; }> = 
  new(...args: ConstructorParameters<T['constructor']>) => InstanceType<T['constructor']>;

const GridConstructor: Constructor<typeof Grid> = Grid.constructor;

This results in the error:

Type 'Grid' does not satisfy the constraint '{ constructor: new (...args: any[]) => any; }'.

TypeScript Playground

2 Answers 2

3

This is currently a limitiation in TypeScript. Class instances do not have strongly-typed constructor properties; as you noted, the compiler only sees its type as Function. There is a quite longstanding open issue, microsoft/TypeScript#3841, asking for this to be changed. But the "obvious" fix, where the compiler makes instances of a class named Foo have a constructor property of type typeof Foo, would break things. It's explained in this comment by the language's architect:

The general problem we face here is that lots of valid JavaScript code has derived constructors that aren't proper subtypes of their base constructors. In other words, the derived constructors "violate" the substitution principle on the static side of the class.

For example:

class Foo {
  x: string;
  constructor(x: string) {
    this.x = x.toUpperCase();
  }
}

Let's imagine that every value foo where foo instanceof Foo had a constructor property of type typeof Foo. Then the compiler would allow you to write the following function without a type assertion:

function cloneFoo(foo: Foo): Foo {
  const ctor = foo.constructor as typeof Foo; // need assertion here in current TS
  return new ctor(foo.x); 
}

Then as long as you are just dealing with Foo itself, there wouldn't be a problem:

cloneFoo(new Foo("x")); // okay

But, JavaScript has class hierarchies where subclass constructors can require a different set of parameters than their base classes, and TypeScript also supports this:

class Bar extends Foo {
  y: string;
  constructor(x: string, y: string) {
    super(x);
    this.y = y.toUpperCase();
  }
}

Here, Bar's constructor requires a second parameter, while Foo's does not. But every instance of Bar is also an instance of Foo, according to both the subtitutability principle and to the JS runtime (e.g., new Bar("x", "y") instanceof Foo evaluates to true).

But this leads directly to a problem. If every Bar instance is also a Foo instance, then every Bar instance's constructor property must be a valid typeof Foo. But it isn't:

cloneFoo(new Bar("x", "y")); // error at runtime! y is undefined

We've gained a strongly-typed constructor but got runtime errors in exchange. To prevent these errors, you'd either need to give up on subclass instances being assignable to superclass instances, making extends a misnomer... or you'd have to restrict subclass constructors so that you could require the same parameters as the superclass, which would be a "massive breaking change" since plenty of existing JavaScript class hierachies violate this rule.

So while the current typing of constructor as just Function is kind of useless, it at least doesn't destroy either class hierarchies or the type system, which is nice. It's possible there might be something better than Function, but not anything which you can safely call with new.

Thus the issue has been sitting untouched for years.


Note that in your case, subclasses are not obviously a possibility, since you have a private property which makes things hard to extend. So perhaps one could open a new issue (or comment on the existing one) asking that constructor be strongly-typed on any class with a private property. In some sense this would be creating true final classes in TypeScript... but that has already been proposed and rejected in microsoft/TypeScript#8306.

Anyway, for now, this is the way it is.


If you know it's safe to use a strongly-typed constructor, then a type assertion is a reasonable solution; but this is tantamount to what you're already doing:

const GridConstructor = Grid.constructor as new (size?: number) => typeof Grid;

Another workaround here is to manually declare a strongly-typed constructor property:

const Grid = new class _Grid extends Array<string[]> {
  ["constructor"]: typeof _Grid
  constructor(private readonly size = 20) {
    super(size);
  }
}

(The quotes and brackets around constructor are necessary to avoid errors... and the rename from Grid to _Grid is not necessary but helps avoid confusion about which thing we're talking about).

Of course, this is hardly better than a type assertion; you are forcing yourself to write out the constructor type yourself.


So I'd say the answer here is just no; the compiler intentionally does not allow you to infer the type of the constructor parameters from the type of an instance, and it's not clear when or if this will ever change.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

1

use this way

class Foo {
  x: string;
  constructor(x: string) {
    this.x = x.toUpperCase();
  }
}

const foo = new Foo('hello');

const foo2 = new (foo.constructor as any)("world")

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.