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

Fix equality narrowing and comparable relation for intersections with {} #50735

Merged
merged 4 commits into from Sep 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 18 additions & 22 deletions src/compiler/checker.ts
Expand Up @@ -19191,11 +19191,15 @@ namespace ts {
// parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't
// appear to be comparable to '2'.
if (relation === comparableRelation && target.flags & TypeFlags.Primitive) {
const constraints = sameMap((source as IntersectionType).types, getBaseConstraintOrType);
const constraints = sameMap((source as IntersectionType).types, t => t.flags & TypeFlags.Instantiable ? getBaseConstraintOfType(t) || unknownType : t);
if (constraints !== (source as IntersectionType).types) {
source = getIntersectionType(constraints);
if (source.flags & TypeFlags.Never) {
return Ternary.False;
}
Comment on lines -19194 to +19199
Copy link
Member

Choose a reason for hiding this comment

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

Where do these changes come into play?

Copy link
Member Author

Choose a reason for hiding this comment

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

From the comment right above:

// Source is an intersection. For the comparable relation, if the target is a primitive type we hoist the
// constraints of all non-primitive types in the source into a new intersection. We do this because the
// intersection may further constrain the constraints of the non-primitive types. For example, given a type
// parameter 'T extends 1 | 2', the intersection 'T & 1' should be reduced to '1' such that it doesn't
// appear to be comparable to '2'.

The code previously didn't properly handle unconstrained type parameters, and it didn't properly handle the case where the hoisting causes the constraint to be never (meaning the types have no overlap).

if (!(source.flags & TypeFlags.Intersection)) {
return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false);
return isRelatedTo(source, target, RecursionFlags.Source, /*reportErrors*/ false) ||
isRelatedTo(target, source, RecursionFlags.Source, /*reportErrors*/ false);
}
}
}
Expand Down Expand Up @@ -19516,7 +19520,7 @@ namespace ts {
// the type param can be compared with itself in the target (with the influence of its constraint to match other parts)
// For example, if `T extends 1 | 2` and `U extends 2 | 3` and we compare `T & U` to `T & U & (1 | 2 | 3)`
const constraint = getEffectiveConstraintOfIntersection(source.flags & TypeFlags.Intersection ? (source as IntersectionType).types: [source], !!(target.flags & TypeFlags.Union));
if (constraint && everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself
if (constraint && !(constraint.flags & TypeFlags.Never) && everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself
// TODO: Stack errors so we get a pyramid for the "normal" comparison above, _and_ a second for this
result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState);
}
Expand Down Expand Up @@ -25294,25 +25298,11 @@ namespace ts {
assumeTrue = !assumeTrue;
}
const valueType = getTypeOfExpression(value);
if (((type.flags & TypeFlags.Unknown) || isEmptyAnonymousObjectType(type) && !(valueType.flags & TypeFlags.Nullable)) &&
assumeTrue &&
(operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)
) {
if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive)) {
return valueType;
}
if (valueType.flags & TypeFlags.Object) {
return nonPrimitiveType;
}
if (type.flags & TypeFlags.Unknown) {
return type;
}
}
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
if (valueType.flags & TypeFlags.Nullable) {
if (!strictNullChecks) {
return type;
}
const doubleEquals = operator === SyntaxKind.EqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsToken;
const facts = doubleEquals ?
assumeTrue ? TypeFacts.EQUndefinedOrNull : TypeFacts.NEUndefinedOrNull :
valueType.flags & TypeFlags.Null ?
Expand All @@ -25321,10 +25311,16 @@ namespace ts {
return getAdjustedTypeWithFacts(type, facts);
}
if (assumeTrue) {
const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ?
t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType) :
t => areTypesComparable(t, valueType);
return replacePrimitivesWithLiterals(filterType(type, filterFn), valueType);
if (!doubleEquals && (type.flags & TypeFlags.Unknown || someType(type, isEmptyAnonymousObjectType))) {
if (valueType.flags & (TypeFlags.Primitive | TypeFlags.NonPrimitive) || isEmptyAnonymousObjectType(valueType)) {
return valueType;
}
if (valueType.flags & TypeFlags.Object) {
return nonPrimitiveType;
}
}
const filteredType = filterType(type, t => areTypesComparable(t, valueType) || doubleEquals && isCoercibleUnderDoubleEquals(t, valueType));
return replacePrimitivesWithLiterals(filteredType, valueType);
}
if (isUnitType(valueType)) {
return filterType(type, t => !(isUnitLikeType(t) && areTypesComparable(t, valueType)));
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/controlFlowOptionalChain.types
Expand Up @@ -1243,9 +1243,9 @@ function f15(o: Thing | undefined, value: number) {
}
else {
o.foo;
>o.foo : number
>o.foo : string | number
>o : Thing
>foo : number
>foo : string | number
}
}

Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/intersectionNarrowing.types
Expand Up @@ -59,11 +59,11 @@ function f4<T>(x: T & 1 | T & 2) {

case 1: x; break; // T & 1
>1 : 1
>x : (T & 1) | (T & 2)
>x : T & 1

case 2: x; break; // T & 2
>2 : 2
>x : (T & 1) | (T & 2)
>x : T & 2

default: x; // Should narrow to never
>x : never
Expand Down
101 changes: 101 additions & 0 deletions tests/baselines/reference/unknownControlFlow.errors.txt
Expand Up @@ -316,4 +316,105 @@ tests/cases/conformance/types/unknown/unknownControlFlow.ts(293,5): error TS2345
type NullableFoo = Foo | undefined;

type Bar<T extends NullableFoo> = NonNullable<T>[string];

// Generics and intersections with {}

function fx0<T>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

function fx1<T extends unknown>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

function fx2<T extends {}>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

function fx3<T extends {} | undefined>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

function fx4<T extends {} | null>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

function fx5<T extends {} | null | undefined>(value: T & ({} | null)) {
if (value === 42) {
value; // T & {}
}
else {
value; // T & ({} | null)
}
}

// Double-equals narrowing

function fx10(x: string | number, y: number) {
if (x == y) {
x; // string | number
}
else {
x; // string | number
}
if (x != y) {
x; // string | number
}
else {
x; // string | number
}
}

// Repros from #50706

function SendBlob(encoding: unknown) {
if (encoding !== undefined && encoding !== 'utf8') {
throw new Error('encoding');
}
encoding;
};

function doSomething1<T extends unknown>(value: T): T {
if (value === undefined) {
return value;
}
if (value === 42) {
throw Error('Meaning of life value');
}
return value;
}

function doSomething2(value: unknown): void {
if (value === undefined) {
return;
}
if (value === 42) {
value;
}
}