joshstrike
joshstrike

Reputation: 1823

Using TS conditional types to narrow to numeric properties

I'm playing around with conditional types and trying to get this this to validate. Interestingly, the test() function does validate if I pass "val" as the parameter, and fails if I pass "name" ...which is the expected behavior. However, Typescript apparently doesn't think o[p] can be relied on to be a number, and throws this:

Operator '+=' cannot be applied to types 'number' and 'T[{ [K in keyof T]: T[K] extends number ? K : never; }[keyof T]]'.

Am I misunderstanding the usage somehow? I thought 'never' would prohibit any parameters that were not explicitly a number...

class Test {
    public static SumParam<T>
        (t: T[], p:{ [K in keyof T]: T[K] extends number ? K : never }[keyof T]): number {
        let n:number = 0;
        for (let o of t) {
            n += o[p]; //Operator '+=' cannot be applied to types 'number' and 'T[{ [K in keyof T]: T[K] extends number ? K : never; }[keyof T]]'.
        }
        return (n);
    }
    public test(): void {
        Test.SumParam(
            [{ name: "alice", val: 3 },
            { name: "bob", val: 4 }],
            "val"); //validates
        Test.SumParam(
            [{ name: "alice", val: 3 },
            { name: "bob", val: 4 }],
            "name"); //Argument of type '"name"' is not assignable to parameter of type '"val"'.
    }
}

The fact that the compiler narrows and recognizes that 'val' is the only enumerable property extending number ...doesn't that imply that the conditional syntax is working...?

Upvotes: 2

Views: 396

Answers (2)

Qwertiy
Qwertiy

Reputation: 21380

I bit other error for name, but in general it works fine:

Playground

class Test {
  public static SumParam<K extends string, T extends { [key in K]: number }>(t: T[], p: K): number {
        let n:number = 0;
        for (let o of t) {
            n += o[p];
        }
        return (n);
    }
    public test(): void {
        Test.SumParam(
            [{ name: "alice", val: 3 },
            { name: "bob", val: 4 }],
            "val"); //validates
        Test.SumParam(
            [{ name: "alice", val: 3 },
            { name: "bob", val: 4 }],
            "name"); // Type 'string' is not assignable to type 'number'.
    }
}

Upvotes: 0

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249476

Typescript will not be able to follow conditional types that still have unresolved type parameters in them. As such it will not be able to know that o[p] is of type number.

If you don't mind having the error on the items in the array, you can type the function in a way that allows typescript to know o[p] is number:

class Test {
    public static SumParam<T extends Record<K, number>, K extends keyof T>
        (t: T[], p: K): number {
        let n:number = 0;
        if (!t) return (n);
        for (let o of t) {
            n += o[p]; //ok.
        }
        return (n);
    }
    public test(): void {
        Test.SumParam(
            [{ name: "alice", val: 3 },
            { name: "bob", val: 4 }],
            "val"); //ok, no error here
        Test.SumParam(
            [{ name: "alice", val: 3 }, // Type 'string' is not assignable to type 'number'.
            { name: "bob", val: 4 }], // Type 'string' is not assignable to type 'number'.
            "name"); 
    }
} 

Upvotes: 3

Related Questions