I'm currently trying to switch from TS 2.6 to 3.4 and I'm running into weird problems. This previously worked, but now it's showing me a compiler error:
type MyNumberType = 'never' | 'gonna';
type MyStringType = 'give' | 'you';
type MyBooleanType = 'up'
interface Bar {
foo(key: MyNumberType): number;
bar(key: MyStringType): string;
baz(key: MyBooleanType): boolean;
}
function test<T extends keyof Bar>(bar: Bar, fn: T) {
let arg: Parameters<Bar[T]>[0];
bar[fn](arg); // error here
}
Error as follows:
Argument of type 'Parameters<Bar[T]>' is not assignable to parameter of type 'never'.
Type 'unknown[]' is not assignable to type 'never'.
Type '[number] | [string] | [boolean]' is not assignable to type 'never'.
Type '[number]' is not assignable to type 'never'.
The Typescript playground tells me that the expected argument of the function is of type 'never':
I don't expect an error here at all. There is only one function argument, and the type of the argument gets inferred via Parameters
. Why is does the function expect never
?
The problem is that the compiler does not understand that arg
and bar[fn]
are correlated. It treats both of them as uncorrelated union types, and thus expects that every combination of union constituents is possible when most combinations are not.
In TypeScript 3.2 you'd've just gotten an error message saying that bar[fn]
doesn't have a call signature, since it is a union of functions with different parameters. I doubt that any version of that code worked in TS2.6; certainly the code with Parameters<>
wasn't in there since conditional types weren't introduced until TS2.8. I tried to recreate your code in a TS2.6-compatible way like
interface B {
foo: MyNumberType,
bar: MyStringType,
baz:MyBooleanType
}
function test<T extends keyof Bar>(bar: Bar, fn: T) {
let arg: B[T]=null!
bar[fn](arg); // error here
}
and tested in TS2.7 but it still gives an error. So I'm going to assume that this code never really worked.
As for the never
issue: TypeScript 3.3 introduced support for calling unions of functions by requiring that the parameters be the intersection of the parameters from the union of functions. That is an improvement in some cases, but in your case it wants the parameter to be the intersection of a bunch of distinct string literals, which gets collapsed to never
. That's basically the same error as before ("you can't call this") represented in a more confusing way.
The most straightforward way for you to deal with this is to use a type assertion, since you are smarter than the compiler in this case:
function test<T extends keyof Bar>(bar: Bar, fn: T) {
let arg: Parameters<Bar[T]>[0] = null!; // give it some value
// assert that bar[fn] takes a union of args and returns a union of returns
(bar[fn] as (x: typeof arg) => ReturnType<Bar[T]>)(arg); // okay
}
A type assertion is not safe, you this does let you lie to the compiler:
function evilTest<T extends keyof Bar>(bar: Bar, fn: T) {
// assertion below is lying to the compiler
(bar[fn] as (x: Parameters<Bar[T]>[0]) => ReturnType<Bar[T]>)("up"); // no error!
}
So you should be careful. There is a way to do a completely type safe version of this, forcing the compiler to do code flow analysis on every possibility:
function manualTest<T extends keyof Bar>(bar: Bar, fn: T): ReturnType<Bar[T]>;
// unions can be narrowed, generics cannot
// see https://github.com/Microsoft/TypeScript/issues/13995
// and https://github.com/microsoft/TypeScript/issues/24085
function manualTest(bar: Bar, fn: keyof Bar) {
switch (fn) {
case 'foo': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
case 'bar': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
case 'baz': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
default:
return assertUnreachable(fn);
}
}
But that is so brittle (requires code changes if you add methods to Bar
) and repetitive (identical clauses over and over) that I usually prefer the type assertion above.
Okay, hope that helps; good luck!
The way barFn
is declared is equivalent to:
type Foo = (key: number) => number;
type Bar = (key: string) => string;
type Baz = (key: boolean) => boolean;
function test1(barFn: Foo | Bar | Baz) {
barFn("a"); // error here
}
barFn
's parameter will be an intersection
of types rather than union
. The type never
is a bottom type and it happens because there is no intersection between number
, string
and boolean
.
Ideally you want to declare barFn
as an intersection of the three function types. e.g:
function test2(barFn: Foo & Bar & Baz) {
barFn("a"); // no error here
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With