Search code examples
typescripttypescript-genericssyntax-checkingtyped

Does the generic type not apply constraint inside the body of a function?


I'm sure if this is possible, I'm beginner in TypeScript but what I'm trying to do is:

I have the following type;

type Special<C> = {
    [Prop in keyof C]: C[Prop] extends Function | string ? C[Prop] : never;
}

There can only be properties of type string and Function, therefore, if there is a property of type number,Array, etc., it will produce an error:

class A {
    public msg: string = 'Hello!';
    public test() {}
}

const a: Special<A> = new A(); // Ok!

But:

class B {
    public msg: string = 'Hello World!';
    public Run() {}
    public num: number = 123;
}

const b: Special<B> = new A(); // Error!, Type 'number' is not assignable to type 'never'.

If I try to do the same inside the body of a function, the type checking is not done:

type Class<ClassType = any, constructorParams = any> = {
    new (...agrs: Array<constructorParams>): ClassType;
};

class A {
    public msg: string = 'Hello!';
    public test() {}
    public num: number = 123;
}

const Testing = <T>(controller: Class<T>) => {
    const a: Special<typeof controller> = new controller(); // is Ok!???
};

Testing(A); // is Ok!???

The only thing I was able to achieve was something like this to get the type checking to work:

class A {
    public msg: string = 'Hello!';
    public test() {}
    public num: number = 123;
}

const Testing = <T>(controller: Class<Special<T>>) => {
    const a = new controller(); 
};

Testing(A); // // Error!, Type 'number' is not assignable to type 'never'.

How can I achieve type checking of "Special " inside the body of the Testing function?

Am I doing something wrong? Am I not understanding something that is missing?

I hope you can help me :)


Solution

  • Based on your comment, it looks like you want the error to be raised in testing's call site. That essentially means that you must constraint the type of the argument testing can take in. And you should constrain it in a way so that the instance returned by a class constructor, is assignable to Special.

    Ok, the bearings you've already provided-

    type Special<C> = {
        [Prop in keyof C]: C[Prop] extends Function | string ? C[Prop] : never;
    }
    

    And here's the class that must not pass without error-

    class A {
        public msg: string = 'Hello!';
        public test() {}
        public num: number = 123;
    }
    

    Here's a similar class that should pass without error-

    class B {
        public msg: string = 'Hello!';
        public test() {}
    }
    

    Ok, now here's the description of your testing function-

    Preliminary

    It takes in a class constructor as its argument.

    Declaration

    Let T be the type of the instance constructed by said class constructor.

    Constraint

    T must be assignable to Special<T>

    Implementation

    The way you constraint T to be assignable to Special<T> is by using T extends Special<T> clause in your function. Combine everything from above, and you get-

    function testing<T extends Special<T>>(controller: new (...args: any[]) => T) {
        const a: Special<T> = new controller();
    };
    

    Nothing too special that hasn't already been discussed. I got rid of your Class type since I just wrote that type inline- new (...args: any[]) => T. Note the T there. That's the instance type.

    Now you'll get an error on testing(A) but not with testing(B). The error you get is-

    Argument of type 'typeof A' is not assignable to parameter of type 'new (...args: any[]) => Special'. Construct signature return types 'A' and 'Special' are incompatible. The types of 'num' are incompatible between these types. Type 'number' is not assignable to type 'never'.

    Check it out on playground