Typescript: Inconsistencies with empty intersection types

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

Two orthogonal problems with empty intersection types:

  • An empty intersection type isn't simplified when it isn't part of a union, which seems inconsistent with what you get by unioning it with itself.
  • A union of empty intersection types always simplifies to never. It should be undefined when strictNullChecks is disabled.

I thought filing one issue might make less work for the TypeScript team. If you agree with the proposed changes, this should be an easy pull request (I've already looked at the code involved) and I'll be happy to submit it.


TypeScript Version: master (d9ed917)

Search Terms: empty intersection unit type undefined union never strictNullChecks

Code

type T = "foo" & "bar";  // "foo" & "bar" (expected undefined)
type TT = T | T;         // never         (expected undefined)
let x: T;
let xx: TT;
let u: undefined;
x = u;
u = x;  // error (expected OK)
u = xx;
x = xx;
xx = u;  // error (expected OK)
xx = x;  // error (expected OK)

Expected behavior: x, xx have the same type, and (assuming strictNullChecks is disabled) that type is undefined.

Actual behavior: As marked.

Playground Link: link

Related Issues: None found

Working as Intended

All 7 comments

I may have missed something, but where are you getting that "foo" & "bar" should become "undefined"? I've never seen that mentioned anywhere.

I think you meant never. undefined is a non-vacuous type it has one value undefined. never is the empty type. so invalid intersections reduce to never.

The inconsistency here is because intersections are only reduced when used in a union. so "foo" & "bar" will stay that type unless it is part of a union. that is why x is not assignable to u.

We have found that keeping the intersections longer gives better error messages to users, since never can does not tell you where the type originated from. more over, there is something to say about user typing a type as such, what the intent is..

I think you meant never. undefined is a non-vacuous type it has one value undefined. never is the empty type. so invalid intersections reduce to never.

When strictNullChecks is off, "foo" really means "foo" | null | undefined. Since null and undefined belong to both "foo" and "bar", they should belong to the intersection "foo" & "bar". So if "foo" & "bar" | "foo" & "bar" simplifies at all, it should simplify to something containing null and undefined. I suggested undefined; null would be equivalent.

We have found that keeping the intersections longer gives better error messages to users ...

OK re this part. Thanks for taking the time to explain.

When strictNullChecks is off, "foo" really means "foo" | null | undefined. Since null and undefined belong to both "foo" and "bar", they should belong to the intersection "foo" & "bar". So if "foo" & "bar" | "foo" & "bar" simplifies at all, it should simplify to something containing null and undefined. I suggested undefined; null would be equivalent.

we consider null and undefined to be implicitly part of the domain of other types, instead of considering every type to be T | null | undefined. this changes the calculus a bit. so (number & string) | never is never and not null | undefined.

this changes the calculus a bit. so (number & string) | never is never and not null | undefined.

I confess I don't understand how you come to this conclusion. If #12825 is implemented, couldn't this lead (under admittedly unlikely circumstances) to a call to a generic function unexpectedly stopping the control flow when the function was supposed to return undefined? But if you say so, feel free to close the issue.

We did not have union types, and we did not have manifest types for null or undefined. they both were always widened to any. We added union types, and a mode to enable checking for null and undefined. to limit disruptions and breaks, the non --strictNullChecks mode had to stay where it was. and null | undefined being in the domain of any type was how it was.

@mhegazy

The inconsistency here is because intersections are only reduced when used in a union. so "foo" & "bar" will stay that type unless it is part of a union. that is why x is not assignable to u.

Were there any other related discussions?

It's very counterintuitive that T extends never and (T|T) extends never can be different.

Was this page helpful?
0 / 5 - 0 ratings