I have a function that accepts two parameters, the first one is the key
I want to access from a given type and the second one should be the values that this given type and key have, for example, given the following type:
type Example = {
letter: 'a' | 'b' | 'c';
number: 1 | 2 | 3;
}
I want these scenarios to be VALID
:
myFunction<Example>('letter', 'a');
myFunction<Example>('letter', 'c');
myFunction<Example>('number', 1);
myFunction<Example>('number', 2);
And these scenarios to be INVALID
:
myFunction<Example>('letter', 1);
myFunction<Example>('letter', 2);
myFunction<Example>('number', 'b');
myFunction<Example>('number', 'c');
Currently I have something that looks similar to this:
type MyProperties = {
size: 'small' | 'medium' | 'large';
type: 'a' | 'b' | 'c';
}
/**
* The second parameter type is not valid because it is accepting all the
* possible values 'small', 'medium', 'large', 'a', 'b' or 'c' even when
* the key is 'size' for example.
*/
function testing<P>( key: keyof P, value: P[typeof key] ) {
//
}
// The current scenario gives me the following:
testing<MyProperties>('size', 'small'); // OK
testing<MyProperties>('size', 'medium'); // OK
testing<MyProperties>('size', 'large'); // OK
testing<MyProperties>('size', 'a'); // OK.
testing<MyProperties>('size', 'b'); // OK.
testing<MyProperties>('size', 'c'); // OK.
testing<MyProperties>('size', 'something'); // BAD.
// But what I really want to achieve is:
testing<MyProperties>('size', 'small'); // OK
testing<MyProperties>('size', 'medium'); // OK
testing<MyProperties>('size', 'large'); // OK
testing<MyProperties>('size', 'a'); // BAD because 'a' is not a "size" valid value even tho is valid for P[typeof key].
testing<MyProperties>('size', 'b'); // BAD because 'b' is not a "size" valid value even tho is valid for P[typeof key].
testing<MyProperties>('size', 'c'); // BAD because 'c' is not a "size" valid value even tho is valid for P[typeof key].
testing<MyProperties>('size', 'something'); // BAD because 'something' is not a "size" valid value.
Is this even possible?
TypeScript currently lacks the ability to have partial generic type argument inference, as requested in microsoft/TypeScript#26242. So while conceptually it would be nice to write
declare function myFunction<T extends object, K extends keyof T>(k: K, v: T[K]): void;
you can't call that in a way that lets you manually specify T
but has the compiler infer K
:
myFunction<Example>('letter', 'a'); // error!
// ~~~~~~~
// Expected 2 type arguments, but got 1.(2558)
You could call it by manually specifying both T
and K
:
myFunction<Example, 'letter'>('letter', "a"); // okay
but that's annoying/redundant.
Until and unless microsoft/TypeScript#26242, you'll need to work around it. In general, people work around that by currying:
declare function myFunction<T extends object>(): <K extends keyof T>(k: K, v: T[K]) => void;
myFunction<Example>()('letter', "a"); // okay
This works but involves an extra function call you don't want. Still, if you need the function to still be generic in K
after specifying T
, you have to do something like this.
But maybe you don't need the function to be generic in K
. Since K
is only meant to be one of a union of types, you can try representing the function's parameters as a union of tuple-typed rest parameters:
type ArgsForMyFunction<T> = { [K in keyof T]: [k: K, v: T[K]] }[keyof T];
declare function myFunction<T extends object>(...[k, v]: ArgsForMyFunction<T>): void;
Here, the ArgsForMyFunction<T>
is a distributive object type as coined in microsoft/TypeScript#47109. That's where you write a mapped type over a set of keys and then index into it with the same set of keys, producing a union of properties. If you have a type function like F<K>
, then type G<K> = {[P in K]: F<P>}[K]
distributes F
over unions in K
, such that G<K1 | K2>
is F<K1> | F<K2>
.
So, in ArgsForMyFunction<T>
, the parameter list tuple [k: K, v: T[K]]
is distributed over the union in keyof T
. Like so:
type Example = {
letter: 'a' | 'b' | 'c';
number: 1 | 2 | 3;
}
type ExampleArgs = ArgsForMyFunction<Example>;
// type ExampleArgs = [k: "number", v: 2 | 1 | 3] | [k: "letter", v: "a" | "b" | "c"]
type TestArgs = ArgsForMyFunction<{ a: string, b: number, c: boolean }>;
// type TestArgs = [k: "a", v: string] | [k: "b", v: number] | [k: "c", v: boolean]
And that means myFunction()
is now only generic in T
and you don't need currying:
myFunction<Example>('letter', 'a'); // okay
myFunction<Example>('letter', 'c'); // okay
myFunction<Example>('number', 1); // okay
myFunction<Example>('number', 2); // okay
myFunction<Example>('letter', 1); // error
myFunction<Example>('letter', 2); // error
myFunction<Example>('number', 'b'); // error
myFunction<Example>('number', 'c'); // error
which is what you wanted.