Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistencies with empty intersection types #25730

Closed
mattmccutchen opened this issue Jul 17, 2018 · 7 comments
Closed

Inconsistencies with empty intersection types #25730

mattmccutchen opened this issue Jul 17, 2018 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@mattmccutchen
Copy link
Contributor

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

@superamadeus
Copy link

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

@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

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

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jul 17, 2018
@mattmccutchen
Copy link
Contributor Author

mattmccutchen commented Jul 17, 2018

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.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

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.

@mattmccutchen
Copy link
Contributor Author

mattmccutchen commented Jul 17, 2018

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.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

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.

@Kinrany
Copy link

Kinrany commented Aug 15, 2018

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants