diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c7774d2d916e7..913d7f3b2a91a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14227,6 +14227,32 @@ namespace ts { return strictNullChecks ? getGlobalNonNullableTypeInstantiation(type) : type; } + + /** + * Is source potentially coercible to target type under `==`. + * Assumes that `source` is a constituent of a union, hence + * the boolean literal flag on the LHS, but not on the RHS. + * + * This does not fully replicate the semantics of `==`. The + * intention is to catch cases that are clearly not right. + * + * Comparing (string | number) to number should not remove the + * string element. + * + * Comparing (string | number) to 1 will remove the string + * element, though this is not sound. This is a pragmatic + * choice. + * + * @see narrowTypeByEquality + * + * @param source + * @param target + */ + function isCoercibleUnderDoubleEquals(source: Type, target: Type): boolean { + return ((source.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.BooleanLiteral)) !== 0) + && ((target.flags & (TypeFlags.Number | TypeFlags.String | TypeFlags.Boolean)) !== 0); + } + /** * Return true if type was inferred from an object literal, written as an object type literal, or is the shape of a module * with no call or construct signatures. @@ -16570,7 +16596,10 @@ namespace ts { return type; } if (assumeTrue) { - const narrowedType = filterType(type, t => areTypesComparable(t, valueType)); + const filterFn: (t: Type) => boolean = operator === SyntaxKind.EqualsEqualsToken ? + (t => areTypesComparable(t, valueType) || isCoercibleUnderDoubleEquals(t, valueType)) : + t => areTypesComparable(t, valueType); + const narrowedType = filterType(type, filterFn); return narrowedType.flags & TypeFlags.Never ? type : replacePrimitivesWithLiterals(narrowedType, valueType); } if (isUnitType(valueType)) { diff --git a/tests/baselines/reference/narrowByEquality.errors.txt b/tests/baselines/reference/narrowByEquality.errors.txt new file mode 100644 index 0000000000000..b391c4a07a1c4 --- /dev/null +++ b/tests/baselines/reference/narrowByEquality.errors.txt @@ -0,0 +1,71 @@ +tests/cases/compiler/narrowByEquality.ts(53,15): error TS2322: Type 'string | number' is not assignable to type 'number'. + Type 'string' is not assignable to type 'number'. +tests/cases/compiler/narrowByEquality.ts(54,9): error TS2322: Type 'string | number' is not assignable to type 'number'. + Type 'string' is not assignable to type 'number'. + + +==== tests/cases/compiler/narrowByEquality.ts (2 errors) ==== + declare let x: number | string | boolean + declare let n: number; + declare let s: string; + declare let b: boolean; + + if (x == n) { + x; + } + + if (x == s) { + x; + } + + if (x == b) { + x; + } + + if (x == 1) { + x; + } + + if (x == "") { + x; + } + + if (x == "foo") { + x; + } + + if (x == true) { + x; + } + + if (x == false) { + x; + } + + declare let xAndObj: number | string | boolean | object + + if (xAndObj == {}) { + xAndObj; + } + + if (x == xAndObj) { + x; + xAndObj; + } + + // Repro from #24991 + + function test(level: number | string):number { + if (level == +level) { + const q2: number = level; // error + ~~ +!!! error TS2322: Type 'string | number' is not assignable to type 'number'. +!!! error TS2322: Type 'string' is not assignable to type 'number'. + return level; + ~~~~~~~~~~~~~ +!!! error TS2322: Type 'string | number' is not assignable to type 'number'. +!!! error TS2322: Type 'string' is not assignable to type 'number'. + } + return 0; + } + \ No newline at end of file diff --git a/tests/baselines/reference/narrowByEquality.js b/tests/baselines/reference/narrowByEquality.js new file mode 100644 index 0000000000000..a843040db7795 --- /dev/null +++ b/tests/baselines/reference/narrowByEquality.js @@ -0,0 +1,101 @@ +//// [narrowByEquality.ts] +declare let x: number | string | boolean +declare let n: number; +declare let s: string; +declare let b: boolean; + +if (x == n) { + x; +} + +if (x == s) { + x; +} + +if (x == b) { + x; +} + +if (x == 1) { + x; +} + +if (x == "") { + x; +} + +if (x == "foo") { + x; +} + +if (x == true) { + x; +} + +if (x == false) { + x; +} + +declare let xAndObj: number | string | boolean | object + +if (xAndObj == {}) { + xAndObj; +} + +if (x == xAndObj) { + x; + xAndObj; +} + +// Repro from #24991 + +function test(level: number | string):number { + if (level == +level) { + const q2: number = level; // error + return level; + } + return 0; +} + + +//// [narrowByEquality.js] +"use strict"; +if (x == n) { + x; +} +if (x == s) { + x; +} +if (x == b) { + x; +} +if (x == 1) { + x; +} +if (x == "") { + x; +} +if (x == "foo") { + x; +} +if (x == true) { + x; +} +if (x == false) { + x; +} +if (xAndObj == {}) { + xAndObj; +} +if (x == xAndObj) { + x; + xAndObj; +} +// Repro from #24991 +function test(level) { + if (level == +level) { + var q2 = level; // error + return level; + } + return 0; +} diff --git a/tests/baselines/reference/narrowByEquality.symbols b/tests/baselines/reference/narrowByEquality.symbols new file mode 100644 index 0000000000000..02e2ae138d98e --- /dev/null +++ b/tests/baselines/reference/narrowByEquality.symbols @@ -0,0 +1,113 @@ +=== tests/cases/compiler/narrowByEquality.ts === +declare let x: number | string | boolean +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + +declare let n: number; +>n : Symbol(n, Decl(narrowByEquality.ts, 1, 11)) + +declare let s: string; +>s : Symbol(s, Decl(narrowByEquality.ts, 2, 11)) + +declare let b: boolean; +>b : Symbol(b, Decl(narrowByEquality.ts, 3, 11)) + +if (x == n) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +>n : Symbol(n, Decl(narrowByEquality.ts, 1, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == s) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +>s : Symbol(s, Decl(narrowByEquality.ts, 2, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == b) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +>b : Symbol(b, Decl(narrowByEquality.ts, 3, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == 1) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == "") { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == "foo") { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == true) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +if (x == false) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +} + +declare let xAndObj: number | string | boolean | object +>xAndObj : Symbol(xAndObj, Decl(narrowByEquality.ts, 37, 11)) + +if (xAndObj == {}) { +>xAndObj : Symbol(xAndObj, Decl(narrowByEquality.ts, 37, 11)) + + xAndObj; +>xAndObj : Symbol(xAndObj, Decl(narrowByEquality.ts, 37, 11)) +} + +if (x == xAndObj) { +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) +>xAndObj : Symbol(xAndObj, Decl(narrowByEquality.ts, 37, 11)) + + x; +>x : Symbol(x, Decl(narrowByEquality.ts, 0, 11)) + + xAndObj; +>xAndObj : Symbol(xAndObj, Decl(narrowByEquality.ts, 37, 11)) +} + +// Repro from #24991 + +function test(level: number | string):number { +>test : Symbol(test, Decl(narrowByEquality.ts, 46, 1)) +>level : Symbol(level, Decl(narrowByEquality.ts, 50, 14)) + + if (level == +level) { +>level : Symbol(level, Decl(narrowByEquality.ts, 50, 14)) +>level : Symbol(level, Decl(narrowByEquality.ts, 50, 14)) + + const q2: number = level; // error +>q2 : Symbol(q2, Decl(narrowByEquality.ts, 52, 13)) +>level : Symbol(level, Decl(narrowByEquality.ts, 50, 14)) + + return level; +>level : Symbol(level, Decl(narrowByEquality.ts, 50, 14)) + } + return 0; +} + diff --git a/tests/baselines/reference/narrowByEquality.types b/tests/baselines/reference/narrowByEquality.types new file mode 100644 index 0000000000000..833e7537d68e8 --- /dev/null +++ b/tests/baselines/reference/narrowByEquality.types @@ -0,0 +1,132 @@ +=== tests/cases/compiler/narrowByEquality.ts === +declare let x: number | string | boolean +>x : string | number | boolean + +declare let n: number; +>n : number + +declare let s: string; +>s : string + +declare let b: boolean; +>b : boolean + +if (x == n) { +>x == n : boolean +>x : string | number | boolean +>n : number + + x; +>x : string | number | boolean +} + +if (x == s) { +>x == s : boolean +>x : string | number | boolean +>s : string + + x; +>x : string | number | boolean +} + +if (x == b) { +>x == b : boolean +>x : string | number | boolean +>b : boolean + + x; +>x : string | number | boolean +} + +if (x == 1) { +>x == 1 : boolean +>x : string | number | boolean +>1 : 1 + + x; +>x : 1 +} + +if (x == "") { +>x == "" : boolean +>x : string | number | boolean +>"" : "" + + x; +>x : "" +} + +if (x == "foo") { +>x == "foo" : boolean +>x : string | number | boolean +>"foo" : "foo" + + x; +>x : "foo" +} + +if (x == true) { +>x == true : boolean +>x : string | number | boolean +>true : true + + x; +>x : true +} + +if (x == false) { +>x == false : boolean +>x : string | number | boolean +>false : false + + x; +>x : false +} + +declare let xAndObj: number | string | boolean | object +>xAndObj : string | number | boolean | object + +if (xAndObj == {}) { +>xAndObj == {} : boolean +>xAndObj : string | number | boolean | object +>{} : {} + + xAndObj; +>xAndObj : object +} + +if (x == xAndObj) { +>x == xAndObj : boolean +>x : string | number | boolean +>xAndObj : string | number | boolean | object + + x; +>x : string | number | boolean + + xAndObj; +>xAndObj : string | number | boolean +} + +// Repro from #24991 + +function test(level: number | string):number { +>test : (level: string | number) => number +>level : string | number + + if (level == +level) { +>level == +level : boolean +>level : string | number +>+level : number +>level : string | number + + const q2: number = level; // error +>q2 : number +>level : string | number + + return level; +>level : string | number + } + return 0; +>0 : 0 +} + diff --git a/tests/cases/compiler/narrowByEquality.ts b/tests/cases/compiler/narrowByEquality.ts new file mode 100644 index 0000000000000..a0ebe09a9a9a7 --- /dev/null +++ b/tests/cases/compiler/narrowByEquality.ts @@ -0,0 +1,59 @@ +// @strict: true + +declare let x: number | string | boolean +declare let n: number; +declare let s: string; +declare let b: boolean; + +if (x == n) { + x; +} + +if (x == s) { + x; +} + +if (x == b) { + x; +} + +if (x == 1) { + x; +} + +if (x == "") { + x; +} + +if (x == "foo") { + x; +} + +if (x == true) { + x; +} + +if (x == false) { + x; +} + +declare let xAndObj: number | string | boolean | object + +if (xAndObj == {}) { + xAndObj; +} + +if (x == xAndObj) { + x; + xAndObj; +} + +// Repro from #24991 + +function test(level: number | string):number { + if (level == +level) { + const q2: number = level; // error + return level; + } + return 0; +}