I want to define a function that can only be called with a specific combination of parameters and to get proper autocompletion for each parameter. A simplified example is this:
interface MyFunction {
(key: "one"): void;
(key: "two", param: Record<"paramForTwo",string>): void;
(key: "three", param: Record<"paramForThree", string>): void;
}
const t: MyFunction = () => { /* implementation is not relevant */ };
This correctly allows
fn("one");
fn("two", { "paramForTwo": "a"})
fn("three", { "paramForThree": "b"})
and rejects
fn("two", { "paramForThree": "c"})
However, in the auto-completion for both WebStorm and VSCode, when I enter
fn("three", "
I get suggestions for both paramForTwo
and paramForThree
.
Even worse, the error message enumerates all possible overloads
error TS2769: No overload matches this call.
Overload 1 of 3, '(key: "two", param: Record<"paramForTwo", string>): void', gave the following error.
Argument of type '"three"' is not assignable to parameter of type '"two"'.
Overload 2 of 3, '(key: "three", param: Record<"paramForThree", string>): void', gave the following error.
Object literal may only specify known properties, and '"paramForTwo"' does not exist in type 'Record<"paramForThree", string>'.
When there are more overloads, the error message does not count all of the, just the last.
I have also tried define types like the addEventListener
types in the DOM library:
interface MyParams {
"one": never,
"two": "paramForTwo",
"three": "paramForThree",
}
function fn<T extends keyof MyParams>(key: T, param: Record<MyParams[T], string>) {}
With this approach, I get proper autocompetion
and the error message is much better:
fn("two", { "paramForThree": "b" })
now shows
error TS2353: Object literal may only specify known properties, and '"paramForThree"' does not exist in type 'Record<"paramForTwo", string>'
But now
fn("one");
also shows an error, because the second parameter is missing. Is there any way to define the function such that it accepts all variants for MyFunction
and computes auto-completion based on the first parameter like in my second approach?
As you've noticed, the IntelliSense for autocompletion of overloads isn't great. You've run into micorosft/TypeScript#26892, and for now that issue is open.
Generic functions generally behave better for IntelliSense, since inference can help restrict the expected input types. And you can generally emulate overloads with generic call signatures, especially if you use conditional types to deal with call signatures that don't have a simple pattern to them, especially if you use rest parameters to deal with call signatures of different parameter lengths with tuple types. Given your MyParams
, you could use a conditional type to detect if the property type is never
and choose the length of the rest parameter based on that:
interface MyParams {
"one": never,
"two": "paramForTwo",
"three": "paramForThree",
}
function fn<K extends keyof MyParams>(
key: K,
...[param]: MyParams[K] extends never ? [] : [param: Record<MyParams[K], string>]
) { }
Here if K
is "one"
then MyParams[K] extends never
is true, and thus the rest parameter is empty, so you can only call fn("one")
. Otherwise, the test is false, and the rest parameter is the one-tuple [param: Record<MyParams[K], string>]
, so you have to call fn("two", {paramForTwo: ""})
.
So that works as asked.
Of course if you're in control of MyParams
, you can just start with tuple types directly:
interface MyParams {
one: [],
two: [param: Record<"paramForTwo", string>]
three: [param: Record<"paramForThree", string>]
}
And then your function doesn't need conditional types, since the indexed access already is of a rest tuple type:
function fn<K extends keyof MyParams>(key: K, ...[param]: MyParams[K]) { }
That's probably how I'd proceed, at least for the example as written in the question.