2

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...

  • Your `SimpleTest` is a [*distributive* conditional type](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types) so `SimpleTest<'test' | 'rest'>` is equivalent to `SimpleTest<'test'> | SimpleTest<'rest'>` and is therefore a union of function types. A union of functions can only be safely called with an intersection of their parameters, and `'test' & 'rest'` is `never` because there is no value which is both those strings. – jcalz Aug 09 '22 at 16:22
  • ...If you don't want a distributive conditional type, you can prevent the union distribution like [this](https://tsplay.dev/m3PQqW). Does that fully address your question? If so I can write up an answer. If not, what am I missing? – jcalz Aug 09 '22 at 16:23
  • @jcalz Wow, that works just like a magic! Thank you very much, it's exactly what I've needed. Please write your comment as an answer and I will mark it. – Pavel Karpovich Aug 09 '22 at 16:26
  • Will do, it might be an hour or two before I get to it – jcalz Aug 09 '22 at 16:27

1 Answers1

3

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
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360