I can think of two approaches.
The first is to make multiParams()
accept a rest parameter of a union of tuple types which we can immediately destructure into correlated variables type
and value
. It could look like this:
type MultiParamsArgs = Resource extends infer R ? R extends Resource ?
[type: R['type'], value: R['value']]
: never : never;
/* type MultiParamsArgs =
[type: "A", value: "A1" | "A2" | "A3"] |
[type: "B", value: "B1" | "B2"];
*/
function multiParams(...[type, value]: MultiParamsArgs) { }
Here I use a distributive conditional type over the union in Resource
, to produce the MultiParamsArgs
type, which you can see is the union of acceptable types for the argument list of multiParams()
.
And indeed, calls to multiParams()
will accept correct parameters and reject incorrect ones:
multiParams('A', 'A1');
multiParams('A', 'B1'); // error
multiParams(Math.random() < 0.999 ? "A" : "B", "B1") // error here too
A benefit to this approach is that the type checker will interpret the type
/value
pair of variables as pieces of a discriminated union, and you can do narrowing on them accordingly:
function multiParams(...[type, value]: MultiParamsArgs) {
if (type === "A") {
const x = { A1: 1, A2: 2, A3: 3 }[value] // okay
}
}
A drawback is that IntelliSense support isn't great; in some sense a union of rest tuples acts like an overloaded function, but you will sometimes only shown the "first" overload in your editor. There's an open issue at microsoft/TypeScript#31977 asking for improvement there, but for now this is a limitation.
The second approach is to make multiParams()
generic in the key type K
of a helper mapping type MultiParamsMap
, so we can represent the connection between the type K
of type
and the indexed access type MultiParamsMap[K]
of value
. It could look like this:
type MultiParamsMap = { [T in Resource as T['type']]: T['value'] }
/* type MultiParams = {
A: "A1" | "A2" | "A3";
B: "B1" | "B2";
} */
function multiParams<K extends keyof MultiParamsMap>(
type: K, value: MultiParamsMap[K]) { }
So MultiParamsMap
is a mapped type with remapped keys over Resource
. For most normal calls of multiParams()
, this accepts correct arguments and rejects incorrect ones.
multiParams('A', 'A1');
multiParams('A', 'B1'); // error
And IntelliSense is decidedly useful; as soon as you write multiParams('A',
the compiler infers "A"
for K
and then prompts you to provide "A1" | "A2" | "A3"
for the second parameter. So that's a benefit.
The drawback here is that the type checker is unable to understand that checking type
has any effect on value
, so you can't use them like a discriminated union:
function multiParams<K extends keyof MultiParamsMap>(
type: K, value: MultiParamsMap[K]) {
if (type === "A") {
const x = { A1: 1, A2: 2, A3: 3 }[value] // error!
}
}
And in fact it is technically possible to pass in a value
that doesn't correspond to type
the way you want:
multiParams(Math.random() < 0.999 ? "A" : "B", "B1") // okay?!
That is accepted because K
is inferred as the union "A" | "B"
, and so MultiParamsMap[K]
is the full "A1" | "A2" | "A3" | "B1" | "B2"
union. And thus "B1"
is accepted as "value"
even though there's a 99.9% chance that type
is "A"
. This sort of thing doesn't happen often in practice, so you probably don't have to worry about it from the caller side, but this possibility is one reason why the implementation narrowing doesn't happen. There are open issues about this at microsoft/TypeScript#27808 and microsoft/TypeScript#33014, but for now this is a limitation.
If IntelliSense is the most important concern for you, then you probably want the generic function solution.
Playground link to code