Call `new` on a variable cast to a type in TypeScript

Viewed 437

I'm writing some unit tests and I want to test that my constructor fails with invalid arguments at runtime. If the constructor is called with invalid arguments in a TS project, it fails at compile time, so I first must cast it to any.

my-class.ts

export default class MyClass {
    constructor(public value: 1 | 2 | 3) {
        if (!this.value || 
            isNaN(this.value) || 
            this.value < 1 || this.value > 3 || 
            parseInt(this.value) !== this.value) {
            throw new Error(`Invalid value: '${this.value}'`);
        }
    }
}

my-class.test.ts

test('MyClass validates its constructor arguments', () => {
    expect(() => new (MyClass as any)()).toThrowErrorMatchingSnapshot();
    expect(() => new (MyClass as any)('abc')).toThrowErrorMatchingSnapshot();
    expect(() => new (MyClass as any)(null)).toThrowErrorMatchingSnapshot();
    // ...
});

I would like to clean this up so I only make one cast, by setting a local variable.

test('MyClass validates its constructor arguments', () => {
    // this type declaration isn't right
    const MC = MyClass as any as { new: (...args: any[]) => MyClass };

    expect(() => new MC()).toThrowErrorMatchingSnapshot();
    expect(() => new MC('abc')).toThrowErrorMatchingSnapshot();
    expect(() => new MC(null)).toThrowErrorMatchingSnapshot();
    // ...
});

I vaguely remember writing a generic that had an type parameter expecting a class, and I thought this was how I wrote it--an object with a function called new, but I now can't find where I found that on the web, and I don't even remember which project it was I wrote it in in the first place.

At any rate, with this code TypeScript yells at me:

Cannot use 'new' with an expression whose type lacks a call or construct signature.ts(2351)

I also tried changing { new: ... } to { constructor: ... } with the same result.

I can leave MC as any which is fine for unit testing, but I'd like to know how to do this in the future.

2 Answers
test('MyClass validates its constructor arguments', () => {

    type Constructor<T> = new (...args: any[]) => T;
    const MC = MyClass as Constructor<MyClass>;

    expect(() => new MC()).toThrowErrorMatchingSnapshot();
    expect(() => new MC('abc')).toThrowErrorMatchingSnapshot();
    expect(() => new MC(null)).toThrowErrorMatchingSnapshot();
    // ...
});

It's also possible to do this inline, though it seems clearer with the explanatory variable.

const MC = MyClass as new (...args: any[]) => MyClass;

Here are both options in the playground.

Turns out I was close. The TypeScript authors chose a kind of weird syntax for this kind of declaration, but I can't honestly say I could come up with something better.

const MC = MyClass as any as new(...args[]: any[]) => MyClass;
Related