Skip to content

Empty intersection reduction #31838

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

Merged
merged 7 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 25 additions & 35 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9522,44 +9522,13 @@ namespace ts {
return false;
}

// Return true if the given intersection type contains
// more than one unit type or,
// an object type and a nullable type (null or undefined), or
// a string-like type and a type known to be non-string-like, or
// a number-like type and a type known to be non-number-like, or
// a symbol-like type and a type known to be non-symbol-like, or
// a void-like type and a type known to be non-void-like, or
// a non-primitive type and a type known to be primitive.
function isEmptyIntersectionType(type: IntersectionType) {
let combined: TypeFlags = 0;
for (const t of type.types) {
if (t.flags & TypeFlags.Unit && combined & TypeFlags.Unit) {
return true;
}
combined |= t.flags;
if (combined & TypeFlags.Nullable && combined & (TypeFlags.Object | TypeFlags.NonPrimitive) ||
combined & TypeFlags.NonPrimitive && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) ||
combined & TypeFlags.StringLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) ||
combined & TypeFlags.NumberLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) ||
combined & TypeFlags.BigIntLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.BigIntLike) ||
combined & TypeFlags.ESSymbolLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) ||
combined & TypeFlags.VoidLike && combined & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) {
return true;
}
}
return false;
}

function addTypeToUnion(typeSet: Type[], includes: TypeFlags, type: Type) {
const flags = type.flags;
if (flags & TypeFlags.Union) {
return addTypesToUnion(typeSet, includes, (<UnionType>type).types);
}
// We ignore 'never' types in unions. Likewise, we ignore intersections of unit types as they are
// another form of 'never' (in that they have an empty value domain). We could in theory turn
// intersections of unit types into 'never' upon construction, but deferring the reduction makes it
// easier to reason about their origin.
if (!(flags & TypeFlags.Never || flags & TypeFlags.Intersection && isEmptyIntersectionType(<IntersectionType>type))) {
// We ignore 'never' types in unions
if (!(flags & TypeFlags.Never)) {
includes |= flags & TypeFlags.IncludesMask;
if (flags & TypeFlags.StructuredOrInstantiable) includes |= TypeFlags.IncludesStructuredOrInstantiable;
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
Expand Down Expand Up @@ -9783,13 +9752,18 @@ namespace ts {
}
}
else {
includes |= flags & TypeFlags.IncludesMask;
if (flags & TypeFlags.AnyOrUnknown) {
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
}
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !contains(typeSet, type)) {
if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) {
// We have seen two distinct unit types which means we should reduce to an
// empty intersection. Adding TypeFlags.NonPrimitive causes that to happen.
includes |= TypeFlags.NonPrimitive;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counterexample:

enum A {
  Zero = 0,
  One = 1
}

type Zeroish<T> = T & 0;
type ShouldNotBeNever = Zeroish<A.Zero> | never;

we handle this incorrectly in master already - I'm just not a fan of propagating the logical error.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is the reason for that that the code highlighted considers A.Zero and 0 to be distinct? I'm just trying to ascertain where in that example above the never creeps in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weswigham I think it is debatable whether we want to consider literals and literal enum members with the same underlying value to be intersectable. But either way, it is an issue that should be covered in a separate PR. Here we're just concerned with changing when we reduce, not how.

}
typeSet.push(type);
}
includes |= flags & TypeFlags.IncludesMask;
}
return includes;
}
Expand Down Expand Up @@ -9905,7 +9879,23 @@ namespace ts {
function getIntersectionType(types: ReadonlyArray<Type>, aliasSymbol?: Symbol, aliasTypeArguments?: ReadonlyArray<Type>): Type {
const typeSet: Type[] = [];
const includes = addTypesToIntersection(typeSet, 0, types);
if (includes & TypeFlags.Never) {
// An intersection type is considered empty if it contains
// the type never, or
// more than one unit type or,
// an object type and a nullable type (null or undefined), or
// a string-like type and a type known to be non-string-like, or
// a number-like type and a type known to be non-number-like, or
// a symbol-like type and a type known to be non-symbol-like, or
// a void-like type and a type known to be non-void-like, or
// a non-primitive type and a type known to be primitive.
if (includes & TypeFlags.Never ||
strictNullChecks && includes & TypeFlags.Nullable && includes & (TypeFlags.Object | TypeFlags.NonPrimitive | TypeFlags.IncludesEmptyObject) ||
includes & TypeFlags.NonPrimitive && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NonPrimitive) ||
includes & TypeFlags.StringLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.StringLike) ||
includes & TypeFlags.NumberLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.NumberLike) ||
includes & TypeFlags.BigIntLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.BigIntLike) ||
includes & TypeFlags.ESSymbolLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.ESSymbolLike) ||
includes & TypeFlags.VoidLike && includes & (TypeFlags.DisjointDomains & ~TypeFlags.VoidLike)) {
return neverType;
}
if (includes & TypeFlags.Any) {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3983,7 +3983,7 @@ namespace ts {
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
// The following flags are aggregated during union and intersection type construction
/* @internal */
IncludesMask = Any | Unknown | Primitive | Never | Object | Union,
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | NonPrimitive,
// The following flags are used for different purposes during union and intersection type construction
/* @internal */
IncludesStructuredOrInstantiable = TypeParameter,
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/indexingTypesWithNever.types
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ declare function genericFn3<

// Should be never
const result5 = genericFn3({ g: "gtest", h: "htest" }, "g", "h"); // 'g' & 'h' will reduce to never
>result5 : unknown
>genericFn3({ g: "gtest", h: "htest" }, "g", "h") : unknown
>result5 : never
>genericFn3({ g: "gtest", h: "htest" }, "g", "h") : never
>genericFn3 : <T extends { [K in keyof T]: T[K]; }, U extends keyof T, V extends keyof T>(obj: T, u: U, v: V) => T[U & V]
>{ g: "gtest", h: "htest" } : { g: string; h: string; }
>g : string
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/inferTypes1.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ tests/cases/conformance/types/conditional/inferTypes1.ts(145,40): error TS2322:
type T50 = X3<{}>; // never
type T51 = X3<{ a: (x: string) => void }>; // never
type T52 = X3<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // string & number
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // never
type T54 = X3<{ a: (x: number) => void, b: () => void }>; // number

type T60 = infer U; // Error
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/inferTypes1.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type X3<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U
type T50 = X3<{}>; // never
type T51 = X3<{ a: (x: string) => void }>; // never
type T52 = X3<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // string & number
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // never
type T54 = X3<{ a: (x: number) => void, b: () => void }>; // number

type T60 = infer U; // Error
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/inferTypes1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ type T52 = X3<{ a: (x: string) => void, b: (x: string) => void }>; // string
>b : Symbol(b, Decl(inferTypes1.ts, 69, 39))
>x : Symbol(x, Decl(inferTypes1.ts, 69, 44))

type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // string & number
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // never
>T53 : Symbol(T53, Decl(inferTypes1.ts, 69, 66))
>X3 : Symbol(X3, Decl(inferTypes1.ts, 63, 52))
>a : Symbol(a, Decl(inferTypes1.ts, 70, 15))
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/inferTypes1.types
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ type T52 = X3<{ a: (x: string) => void, b: (x: string) => void }>; // string
>b : (x: string) => void
>x : string

type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // string & number
>T53 : number & string
type T53 = X3<{ a: (x: number) => void, b: (x: string) => void }>; // never
>T53 : never
>a : (x: number) => void
>x : number
>b : (x: string) => void
Expand Down
66 changes: 66 additions & 0 deletions tests/baselines/reference/intersectionReduction.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
tests/cases/conformance/types/intersection/intersectionReduction.ts(40,1): error TS2322: Type 'any' is not assignable to type 'never'.
tests/cases/conformance/types/intersection/intersectionReduction.ts(41,1): error TS2322: Type 'any' is not assignable to type 'never'.


==== tests/cases/conformance/types/intersection/intersectionReduction.ts (2 errors) ====
declare const sym1: unique symbol;
declare const sym2: unique symbol;

type T1 = string & 'a'; // 'a'
type T2 = 'a' & string & 'b'; // never
type T3 = number & 10; // 10
type T4 = 10 & number & 20; // never
type T5 = symbol & typeof sym1; // typeof sym1
type T6 = typeof sym1 & symbol & typeof sym2; // never
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // never

type T10 = string & ('a' | 'b'); // 'a' | 'b'
type T11 = (string | number) & ('a' | 10); // 'a' | 10

type N1 = 'a' & 'b';
type N2 = { a: string } & null;
type N3 = { a: string } & undefined;
type N4 = string & number;
type N5 = number & object;
type N6 = symbol & string;
type N7 = void & string;

type X = { x: string };

type X1 = X | 'a' & 'b';
type X2 = X | { a: string } & null;
type X3 = X | { a: string } & undefined;
type X4 = X | string & number;
type X5 = X | number & object;
type X6 = X | symbol & string;
type X7 = X | void & string;

// Repro from #31663

const x1 = { a: 'foo', b: 42 };
const x2 = { a: 'foo', b: true };

declare let k: 'a' | 'b';

x1[k] = 'bar' as any; // Error
~~~~~
!!! error TS2322: Type 'any' is not assignable to type 'never'.
x2[k] = 'bar' as any; // Error
~~~~~
!!! error TS2322: Type 'any' is not assignable to type 'never'.

const enum Tag1 {}
const enum Tag2 {}

declare let s1: string & Tag1;
declare let s2: string & Tag2;

declare let t1: string & Tag1 | undefined;
declare let t2: string & Tag2 | undefined;

s1 = s2;
s2 = s1;

t1 = t2;
t2 = t1;

63 changes: 56 additions & 7 deletions tests/baselines/reference/intersectionReduction.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,69 @@
//// [intersectionReduction.ts]
// @strict

declare const sym1: unique symbol;
declare const sym2: unique symbol;

type T1 = string & 'a'; // 'a'
type T2 = 'a' & string & 'b'; // 'a' & 'b'
type T2 = 'a' & string & 'b'; // never
type T3 = number & 10; // 10
type T4 = 10 & number & 20; // 10 & 20
type T4 = 10 & number & 20; // never
type T5 = symbol & typeof sym1; // typeof sym1
type T6 = typeof sym1 & symbol & typeof sym2; // typeof sym1 & typeof sym2
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // 'a' & 10 & typeof sym1
type T6 = typeof sym1 & symbol & typeof sym2; // never
type T7 = string & 'a' & number & 10 & symbol & typeof sym1; // never

type T10 = string & ('a' | 'b'); // 'a' | 'b'
type T11 = (string | number) & ('a' | 10); // 'a' | 10

type N1 = 'a' & 'b';
type N2 = { a: string } & null;
type N3 = { a: string } & undefined;
type N4 = string & number;
type N5 = number & object;
type N6 = symbol & string;
type N7 = void & string;

type X = { x: string };

type X1 = X | 'a' & 'b';
type X2 = X | { a: string } & null;
type X3 = X | { a: string } & undefined;
type X4 = X | string & number;
type X5 = X | number & object;
type X6 = X | symbol & string;
type X7 = X | void & string;

// Repro from #31663

const x1 = { a: 'foo', b: 42 };
const x2 = { a: 'foo', b: true };

declare let k: 'a' | 'b';

x1[k] = 'bar' as any; // Error
x2[k] = 'bar' as any; // Error

const enum Tag1 {}
const enum Tag2 {}

declare let s1: string & Tag1;
declare let s2: string & Tag2;

declare let t1: string & Tag1 | undefined;
declare let t2: string & Tag2 | undefined;

s1 = s2;
s2 = s1;

t1 = t2;
t2 = t1;


//// [intersectionReduction.js]
// @strict
// Repro from #31663
var x1 = { a: 'foo', b: 42 };
var x2 = { a: 'foo', b: true };
x1[k] = 'bar'; // Error
x2[k] = 'bar'; // Error
s1 = s2;
s2 = s1;
t1 = t2;
t2 = t1;
Loading