Skip to content

Commit

Permalink
Fix equality narrowing and comparable relation for intersections with…
Browse files Browse the repository at this point in the history
… {} (#50735)

* Fox equality narrowing and comparable relation for intersections with {}

* Accept new baselines

* Add tests

* Accept new baselines
  • Loading branch information
ahejlsberg committed Sep 14, 2022
1 parent b23f1d6 commit 4110b80
Show file tree
Hide file tree
Showing 8 changed files with 849 additions and 26 deletions.
40 changes: 18 additions & 22 deletions src/compiler/checker.ts
Expand Up @@ -19216,11 +19216,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;
}
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 @@ -19541,7 +19545,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 @@ -25322,25 +25326,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 @@ -25349,10 +25339,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;
}
}

0 comments on commit 4110b80

Please sign in to comment.