Typescript: Weird behavior when together use type alias and extends

Created on 26 Jul 2018  路  7Comments  路  Source: microsoft/TypeScript

code:

type T = "a" | "b" | "c" | "x"
type U = "x" | "y" | "z" | "a"

type a = T extends U ? never : T;  //type a = "a" | "b" | "c" | "x"

type b = Exclude<T, U>              // type b = "b" | "c"

question:

Exclude is defined by < T extends U ? never : T>

type a: T and U act like a whole type
type b: T ,U took in to type Exclude, the behavior was changed, T treated like using the operator "in" to spread T, U just like a whole type

Is there any document or achieve to describe this.

forgive my poor English.

Docs

Most helpful comment

You've discovered distributive conditional types. I agree that the difference in behavior when a type alias is inlined is weird (ideally there would have been a dedicated syntax to mark distributive conditional types), but it's intended.

All 7 comments

I didn't understand at first and tried it myself on the playground:

type T = "a" | "b" | "c" | "x"
type U = "x" | "y" | "z" | "a"

type Ext<R> = R extends U ? never : R;
type T1 = Ext<T>; // "b" | "c"
type T2 = T extends U ? never : T; // "a" | "b" | "c" | "x"

Although T1 and T2 seem to describe the same type, they don't.

You've discovered distributive conditional types. I agree that the difference in behavior when a type alias is inlined is weird (ideally there would have been a dedicated syntax to mark distributive conditional types), but it's intended.

So basically T1 is equal to

Ext<"a"> | Ext<"b"> | Ext<"c"> | Ext<"x">
=> ("a" extends U ? never : "a") | ... | ("x" extends U ? never : "x")
=> "b" | "c"

but T2 is equal to

("a" | "b" | "c" | "x") extends U ? never : ("a" | "b" | "c" | "x") 
=> "a" | "b" | "c" | "x"

because the latter does not go through a generic type alias?

To be fair, the relevant part in the docs could be clearer regarding this distinction. In general that part its very hard to understand and could almost be written in chinese :rofl:
More examples, less technical terms!

ideally there would have been a dedicated syntax to mark distributive conditional types

@mattmccutchen would I be reading this correctly to imply that putting a generic with union parameter inside a condition currently _forces_ the union to distribute over the generic? Does there happen to be a plan allowing for us to opt-out of this behavior?

would I be reading this correctly to imply that putting a generic with union parameter inside a condition currently forces the union to distribute over the generic?

Yes.

Does there happen to be a plan allowing for us to opt-out of this behavior?

Wrap the type parameter and the test type in a tuple type:

type Weird<T> = [T] extends [string] 
    ? WithString<T> 
    : T;

I think I've seen this documented somewhere, but I can't find it now. Anyway, it belongs in the handbook entry for conditional types.

Wrap the type parameter and the test type in a tuple type

Oh, wow... Yeah that definitely belongs in the docs!

Same weird behavior:

type Factory<T = undefined> = T extends undefined
  ? () => void
  : (value: T) => void

let withArgsSimple: Factory<{ data: string }>
let withArgsComplex: Factory<{ data: string } | { value: string }>

withArgsSimple({ data: "" }) // Ok
withArgsComplex({ data: "" }) // Error
Was this page helpful?
0 / 5 - 0 ratings