I found an issue which I don't know how to overcome...
I created a simple type to demonstrate the problem:
type SimpleTest<U> = U extends {something: string}
? () => void
: (a: U) => U
And this type does not work as I wanted with union types:
function test(a: SimpleTest<'test' | 'rest'>) {
a('test'); // Error: TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.
}
There is an error because TypeScript interprets type SimpleTest<'test' | 'rest'>
as (a: never) => ("test" | "rest")
.
But I want it to be (a: "test" | "rest") => ("test" | "rest")
.
What is really surprising for me that the type of the returning value is correct, but the same type in the function parameter is transformed to never
...
I tried some things to find a workaround but without any success...
CodePudding user response:
Your SimpleTest<U>
is (accidentally) a distributive conditional type because it is of the form XXX extends YYY ? AAA : BBB
where XXX
is a generic type parameter. Distributive conditional types distribute over unions in their inputs. So if F<T>
is a distributive conditional types, then F<A | B | C>
is equivalent to F<A> | F<B> | F<C>
: the union input is split into its individual members, the type function is applied to each member, and the result is joined back together in a union.
And so SimpleTest<'test' | 'rest'>
is evaluated as SimpleTest<'test'> | SimpleTest<'rest'>
:
type Test = SimpleTest<'test' | 'rest'>;
// type Test = ((a: "test") => "test") | ((a: "rest") => "rest")
This is a union of function types, and unions of function types aren't generally easy to call. If I handed you a function and said it's either a function that accepts only "test"
or it's a function that accepts only "rest"
but I don't know which one, then you couldn't call it safely with either "test"
or "rest"
. That's a general feature of a union of functions; you can only safely pass it an intersection of the parameter types... if you had a value which was both "rest"
and "test"
, then you could call the function. But there are no values like that; the intersection "rest" & "test"
is the impossible never
type, and that's why you get the error:
a('test'); // error! 'string' is not assignable to 'never'.
When you have a conditional type that's unintentionally distributive, you can make it non-distributive by modifying it so that the checked type is not a generic type parameter itself. The tersest way to do this is to change XXX extends YYY ? AAA : BBB
to [XXX] extends [YYY] ? AAA : BBB
. Technically you're now comparing two single-element tuple type, which are considered covariant in their elements, so [XXX] extends [YYY]
if and only if XXX extends YYY
. But since [XXX]
isn't a generic type parameter (it's a tuple containing such a type parameter), the conditional type is no longer distributive.
So you can make this change:
type SimpleTest<U> = [U] extends [{ something: string }]
? () => void
: (a: U) => U
And now things behave as expected, and SimpleTest<U>
is never a union of functions, no matter what you plug in for U
:
type Test = SimpleTest<'test' | 'rest'>;
// type Test = (a: "test" | "rest") => "test" | "rest"
function test(a: SimpleTest<'test' | 'rest'>) {
a('test'); // okay
}