TDD_hobbyist
TDD_hobbyist

Reputation: 23

How to get generic factory to typecheck constructor of different classes?

Given this example:

    class OneArgConstructorClass {
        constructor(name: string) {}
    }
    class TwoArgConstructorClass {
        constructor(name: string, age: number) {}
    }
    class Creator<T>
    {
        constructor(tConstructor: new(...args: any[])=> T) {
            const it: T = new tConstructor();
        }
    }

    const e1 = new Creator<OneArgConstructorClass>(OneArgConstructorClass); 
    const e2 = new Creator<OneArgConstructorClass>(TwoArgConstructorClass); 

Why are e1 and e2 valid statements? Shouldn't typescript complain that it can't correctly construct e1/e2 without the proper number and type of arguments? Is it possible to make this sample properly typechecked against the passed in class constructors?

Upvotes: 1

Views: 101

Answers (1)

Aron
Aron

Reputation: 9248

The reason there is no error is because your tConstructor parameter takes any number of parameters of any type.

If you want to be more restrictive about which parameters are allowed you could use tuple types to specify exactly which parameters tConstructor takes, e.g.:

class Creator<T>
{
    constructor(tConstructor: new (...args: [string]) => T) {
    //                                      ^^^^^^^^
    // note that args is a tuple containing exactly one item, namely a string
        const it: T = new tConstructor();
        //            ^^^^^^^^^^^^^^^^^^
        // This is now wrong because the constructor takes a string as parameter
    }
}

const e1 = new Creator<OneArgConstructorClass>(OneArgConstructorClass);
const e2 = new Creator<OneArgConstructorClass>(TwoArgConstructorClass);
//                                             ^^^^^^^^^^^^^^^^^^^^^^^
// This is now also wrong because the constructor signatures are not compatible

If you made ...args's signature [string, number] then your call of new tConstructor() would still be wrong because you haven't given it its 2 required parameters, but new Creator<OneArgConstructorClass>(TwoArgConstructorClass) would be fine as it now receives its correct 2 parameters.

Unlike what you may expect though, new Creator<OneArgConstructorClass>(OneArgConstructorClass) also doesn't throw an error anymore because the constructor that accepts 1 parameter will safely ignore whatever additional parameters it receives.

Hope this helps.

EDIT

If you want a typesafe way of calling the class constructors you need to do something like this:

// Note the constraint that T must be "new"able
class Creator<T extends new (...args: any[]) => any>
{
    // ConstructorParameters is a builtin TS utility that when given a class returns the type of its parameters in a tuple
    // Note that we need to pass the actual parameters in somehow; in this example I've made Creator#constructor take 2 parameters:
    // 1) the class
    // 2) its constructor parameters in a tuple
    constructor(tConstructor: new (...args: ConstructorParameters<T>) => any,
        argsArray: ConstructorParameters<T>) {

        // Construct the class with the arguments array!
        const it: T = new tConstructor(...argsArray);
    }
}

// This will now work
const e1 = new Creator<typeof OneArgConstructorClass>(OneArgConstructorClass, ['hello']);

// This will now *not* work, which is correct, as the constructor signatures don't match
const e2 = new Creator<typeof OneArgConstructorClass>(TwoArgConstructorClass, ['hello', 14]);
//                                                    ^^^^^^^^^^^^^^^^^^^^^^
// Argument of type 'typeof TwoArgConstructorClass' is not assignable to parameter of type 'new (name: string) => any'.

// This *will* work because the signatures match
const e3 = new Creator<typeof TwoArgConstructorClass>(TwoArgConstructorClass, ['hello', 14]); 

Upvotes: 1

Related Questions