Search code examples
typescript

Why keyof a picked type is not picked keys in function generic?


interface Test {
    a: string;
    b: number;
    c: boolean;
}


let arr: string[] = []

function test<S extends Pick<Test, 'a' | 'b'>, T extends keyof S>(val: T[]) {
    arr = val // it's not ok
}

enter image description here

PlayGround: https://www.typescriptlang.org/play?ssl=19&ssc=27&pln=15&pc=1#code/JYOwLgpgTgZghgYwgAgCoQM5mQbwFDKHJwBcyWUoA5gNwFEBGZIArgLYPR1HIJkMB7AQBsIcEHQC+eGaOxwoUMhWoBtALrIAvMg0yYLEAjDABIZJCwAeAMrIIAD0ggAJhmQAFYAgDWV9FgANMgA5HAhyAA+oQwhAHzBqPZOEK7uPhAAngIwyDZxABQAbnDCZKgaAJS49IQKUNrIJcLIAPStyIaOSAAOkC72igJQAIR40jJgmT0odjpevv6YYMFhEdEhsXF0UzNojRnZuTYyCGZYTaXlGo169Y3NbR0A7gAWmcjA2MDuAj4A-EA

But, if I don't use function, it's ok:

type S = Pick<Test, 'a' | 'b'>;
type T = keyof S

const val: T[] = []
arr = val // why it is ok?

And, When I declare the generic S separately, there is also no error:

type S = Pick<Test, 'a' | 'b'>;

function test<T extends keyof S>(val: T[]) {
    arr = val // it is ok
}

Solution

  • This is because the generic type S can have more keys than just "a" and "b" — that's what the extends keyword means in that position: that the generic type S must be constrained by Pick<Test, "a" | "b"> (not that it only has keys "a" and "b").

    TypeScript is structurally-typed (more here and here), so supplying a generic type that satisfies Pick<Test, "a" | "b"> that also has an index signature of symbol keys with unknown values will be accepted by the compiler…

    TS Playground

    type Foo = {
      a: string;
      b: number;
      [key: symbol]: unknown;
    };
    
    const input: (keyof Foo)[] = [Symbol("foo"), "a", "b"];
    
    test<Foo, keyof Foo>(input); // Ok, but symbol is not assignable to string
    

    …but symbol is not assignable to string, so it would be a type error to accept an array of such values (or any other non-string value).

    In the second code block example of your question you provided this code and question:

    type S = Pick<Test, "a" | "b">;
    type T = keyof S;
    
    const val: T[] = [];
    arr = val; // why it is ok?
    

    There, S is a type that looks like { a: string; b: number } and T is the union of its keys, which is "a" | "b". An array of "a" and "b" values are assignable to string, so there's no problem there.

    A modified version of your test function that uses these types would look like this…

    TS Playground

    function test(val: (keyof Pick<Test, "a" | "b">)[]) {
      arr = val;
    }
    

    Note that keyof Pick<Test, "a" | "b"> is just "a" | "b".

    …and attempting to provide an array of values that includes a non-string will produce a compiler diagnostic error:

    type Foo = {
      a: string;
      b: number;
      [key: symbol]: unknown;
    };
    
    const input: (keyof Foo)[] = [Symbol("foo"), "a", "b"];
    
    test(input); /* Error
         ~~~~~
    Argument of type '(keyof Foo)[]' is not assignable to parameter of type '("a" | "b")[]'.
      Type 'keyof Foo' is not assignable to type '"a" | "b"'.
        Type 'symbol' is not assignable to type '"a" | "b"'.(2345) */