diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e47a28359d579..8d502c2b34c2d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -595,6 +595,7 @@ namespace ts { const assignableRelation = createMap(); const definitelyAssignableRelation = createMap(); const comparableRelation = createMap(); + const relaxComparableRelation = createMap(); const identityRelation = createMap(); const enumRelation = createMap(); @@ -10109,6 +10110,14 @@ namespace ts { return checkTypeRelatedTo(source, target, assignableRelation, errorNode, headMessage, containingMessageChain); } + function isTypeRelaxComparableTo(source: Type, target: Type): boolean { + return isTypeRelatedTo(source, target, relaxComparableRelation); + } + + function areTypesRelaxComparable(type1: Type, type2: Type): boolean { + return isTypeRelaxComparableTo(type1, type2) || isTypeRelaxComparableTo(type2, type1); + } + /** * This is *not* a bi-directional relationship. * If one needs to check both directions for comparability, use a second call to this function or 'isTypeComparableTo'. @@ -10371,6 +10380,14 @@ namespace ts { const t = target.flags; if (t & TypeFlags.AnyOrUnknown || s & TypeFlags.Never || source === wildcardType) return true; if (t & TypeFlags.Never) return false; + if (relation === relaxComparableRelation) { + if (s & TypeFlags.NumberLike && t & TypeFlags.StringLike) return true; + if (s & TypeFlags.NumberLike && t & TypeFlags.BooleanLike) return true; + // if (s & TypeFlags.NumberLike && t & TypeFlags.Object) return true; + if (s & TypeFlags.StringLike && t & TypeFlags.BooleanLike) return true; + // if (s & TypeFlags.StringLike && t & TypeFlags.Object) return true; + // if (s & TypeFlags.BooleanLike && t & TypeFlags.Object) return true; + } if (s & TypeFlags.StringLike && t & TypeFlags.String) return true; if (s & TypeFlags.StringLiteral && s & TypeFlags.EnumLiteral && t & TypeFlags.StringLiteral && !(t & TypeFlags.EnumLiteral) && @@ -10392,7 +10409,7 @@ namespace ts { if (s & TypeFlags.Null && (!strictNullChecks || t & TypeFlags.Null)) return true; if (s & TypeFlags.Object && t & TypeFlags.NonPrimitive) return true; if (s & TypeFlags.UniqueESSymbol || t & TypeFlags.UniqueESSymbol) return false; - if (relation === assignableRelation || relation === definitelyAssignableRelation || relation === comparableRelation) { + if (relation === assignableRelation || relation === definitelyAssignableRelation || (relation === comparableRelation || relation === relaxComparableRelation)) { if (s & TypeFlags.Any) return true; // Type number or any numeric literal type is assignable to any numeric enum type or any // numeric enum literal type. This rule exists for backwards compatibility reasons because @@ -10411,7 +10428,7 @@ namespace ts { target = (target).regularType; } if (source === target || - relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + (relation === comparableRelation || relation === relaxComparableRelation) && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || relation !== identityRelation && isSimpleTypeRelatedTo(source, target, relation)) { return true; } @@ -10505,7 +10522,7 @@ namespace ts { } if (!message) { - if (relation === comparableRelation) { + if (relation === comparableRelation || relation === relaxComparableRelation) { message = Diagnostics.Type_0_is_not_comparable_to_type_1; } else if (sourceType === targetType) { @@ -10583,7 +10600,7 @@ namespace ts { return isIdenticalTo(source, target); } - if (relation === comparableRelation && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || + if ((relation === comparableRelation || relation === relaxComparableRelation) && !(target.flags & TypeFlags.Never) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True; if (isObjectLiteralType(source) && source.flags & TypeFlags.FreshLiteral) { @@ -10603,7 +10620,7 @@ namespace ts { } } - if (relation !== comparableRelation && + if ((relation !== comparableRelation || relation === relaxComparableRelation) && !(source.flags & TypeFlags.UnionOrIntersection) && !(target.flags & TypeFlags.Union) && !isIntersectionConstituent && @@ -10634,7 +10651,7 @@ namespace ts { // we need to deconstruct unions before intersections (because unions are always at the top), // and we need to handle "each" relations before "some" relations for the same kind of type. if (source.flags & TypeFlags.Union) { - result = relation === comparableRelation ? + result = (relation === comparableRelation || relation === relaxComparableRelation) ? someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)) : eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive)); } @@ -10755,7 +10772,7 @@ namespace ts { function hasExcessProperties(source: FreshObjectLiteralType, target: Type, discriminant: Type | undefined, reportErrors: boolean): boolean { if (maybeTypeOfKind(target, TypeFlags.Object) && !(getObjectFlags(target) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) { const isComparingJsxAttributes = !!(getObjectFlags(source) & ObjectFlags.JsxAttributes); - if ((relation === assignableRelation || relation === definitelyAssignableRelation || relation === comparableRelation) && + if ((relation === assignableRelation || relation === definitelyAssignableRelation || (relation === comparableRelation || relation === relaxComparableRelation)) && (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { return false; } @@ -11268,7 +11285,7 @@ namespace ts { // related to Y, where X' is an instantiation of X in which P is replaced with Q. Notice // that S and T are contra-variant whereas X and Y are co-variant. function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary { - const modifiersRelated = relation === comparableRelation || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : + const modifiersRelated = (relation === comparableRelation || relation === relaxComparableRelation) || (relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); if (modifiersRelated) { let result: Ternary; @@ -11363,7 +11380,7 @@ namespace ts { } result &= related; // When checking for comparability, be more lenient with optional properties. - if (relation !== comparableRelation && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { + if ((relation !== comparableRelation && relation !== relaxComparableRelation) && sourceProp.flags & SymbolFlags.Optional && !(targetProp.flags & SymbolFlags.Optional)) { // TypeScript 1.0 spec (April 2014): 3.8.3 // S is a subtype of a type T, and T is a supertype of S if ... // S' and T are object types and, for each member M in T.. @@ -11483,7 +11500,7 @@ namespace ts { // in the context of the target signature before checking the relationship. Ideally we'd do // this regardless of the number of signatures, but the potential costs are prohibitive due // to the quadratic nature of the logic below. - const eraseGenerics = relation === comparableRelation || !!compilerOptions.noStrictGenericChecks; + const eraseGenerics = (relation === comparableRelation || relation === relaxComparableRelation) || !!compilerOptions.noStrictGenericChecks; result = signatureRelatedTo(sourceSignatures[0], targetSignatures[0], eraseGenerics, reportErrors); } else { @@ -14157,7 +14174,8 @@ namespace ts { return type; } if (assumeTrue) { - const narrowedType = filterType(type, t => areTypesComparable(t, valueType)); + const strictEquals = operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken; + const narrowedType = filterType(type, t => strictEquals ? areTypesComparable(t, valueType) : areTypesRelaxComparable(t, valueType)); return narrowedType.flags & TypeFlags.Never ? type : replacePrimitivesWithLiterals(narrowedType, valueType); } if (isUnitType(valueType)) { diff --git a/tests/baselines/reference/typeGuardWithRelaxEquality.errors.txt b/tests/baselines/reference/typeGuardWithRelaxEquality.errors.txt new file mode 100644 index 0000000000000..4494a9d8b4ec4 --- /dev/null +++ b/tests/baselines/reference/typeGuardWithRelaxEquality.errors.txt @@ -0,0 +1,59 @@ +tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts(5,9): error TS2322: Type 'string | number' is not assignable to type 'number'. + Type 'string' is not assignable to type 'number'. + + +==== tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts (1 errors) ==== + // Github issue #24991 + function test(level: number | string):number { + if (level == +level) { + const q2 = level; // number | string + return level; + ~~~~~~~~~~~~~ +!!! error TS2322: Type 'string | number' is not assignable to type 'number'. +!!! error TS2322: Type 'string' is not assignable to type 'number'. + } + if (level === +level + 1) { + const q2 = level; + return level; + } + return 0; + } + alert(test(5) + 1); + alert(test("5") + 1) + + declare const a: string | number | boolean | object | symbol | null | undefined; + declare const s: symbol; + declare const str: string; + declare const num: number; + declare const bool: boolean; + + if (a == 1) { + const t = a + } + if (a == num) { + const t = a + } + if (a == '') { + const t = a + } + if (a == str) { + const t = a + } + if (a == false) { + const t = a + } + if (a == bool) { + const t = a + } + if (a == {}) { + const t = a + } + if (a == s) { + const t = a + } + if (a == null) { + const t = a + } + if (a == undefined) { + const t = a + } \ No newline at end of file diff --git a/tests/baselines/reference/typeGuardWithRelaxEquality.js b/tests/baselines/reference/typeGuardWithRelaxEquality.js new file mode 100644 index 0000000000000..e4e52218ed2d4 --- /dev/null +++ b/tests/baselines/reference/typeGuardWithRelaxEquality.js @@ -0,0 +1,99 @@ +//// [typeGuardWithRelaxEquality.ts] +// Github issue #24991 +function test(level: number | string):number { + if (level == +level) { + const q2 = level; // number | string + return level; + } + if (level === +level + 1) { + const q2 = level; + return level; + } + return 0; +} +alert(test(5) + 1); +alert(test("5") + 1) + +declare const a: string | number | boolean | object | symbol | null | undefined; +declare const s: symbol; +declare const str: string; +declare const num: number; +declare const bool: boolean; + +if (a == 1) { + const t = a +} +if (a == num) { + const t = a +} +if (a == '') { + const t = a +} +if (a == str) { + const t = a +} +if (a == false) { + const t = a +} +if (a == bool) { + const t = a +} +if (a == {}) { + const t = a +} +if (a == s) { + const t = a +} +if (a == null) { + const t = a +} +if (a == undefined) { + const t = a +} + +//// [typeGuardWithRelaxEquality.js] +"use strict"; +// Github issue #24991 +function test(level) { + if (level == +level) { + var q2 = level; // number | string + return level; + } + if (level === +level + 1) { + var q2 = level; + return level; + } + return 0; +} +alert(test(5) + 1); +alert(test("5") + 1); +if (a == 1) { + var t = a; +} +if (a == num) { + var t = a; +} +if (a == '') { + var t = a; +} +if (a == str) { + var t = a; +} +if (a == false) { + var t = a; +} +if (a == bool) { + var t = a; +} +if (a == {}) { + var t = a; +} +if (a == s) { + var t = a; +} +if (a == null) { + var t = a; +} +if (a == undefined) { + var t = a; +} diff --git a/tests/baselines/reference/typeGuardWithRelaxEquality.symbols b/tests/baselines/reference/typeGuardWithRelaxEquality.symbols new file mode 100644 index 0000000000000..ef54bfef6a2a6 --- /dev/null +++ b/tests/baselines/reference/typeGuardWithRelaxEquality.symbols @@ -0,0 +1,128 @@ +=== tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts === +// Github issue #24991 +function test(level: number | string):number { +>test : Symbol(test, Decl(typeGuardWithRelaxEquality.ts, 0, 0)) +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + + if (level == +level) { +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + + const q2 = level; // number | string +>q2 : Symbol(q2, Decl(typeGuardWithRelaxEquality.ts, 3, 13)) +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + + return level; +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + } + if (level === +level + 1) { +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + + const q2 = level; +>q2 : Symbol(q2, Decl(typeGuardWithRelaxEquality.ts, 7, 13)) +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + + return level; +>level : Symbol(level, Decl(typeGuardWithRelaxEquality.ts, 1, 14)) + } + return 0; +} +alert(test(5) + 1); +>alert : Symbol(alert, Decl(lib.dom.d.ts, --, --)) +>test : Symbol(test, Decl(typeGuardWithRelaxEquality.ts, 0, 0)) + +alert(test("5") + 1) +>alert : Symbol(alert, Decl(lib.dom.d.ts, --, --)) +>test : Symbol(test, Decl(typeGuardWithRelaxEquality.ts, 0, 0)) + +declare const a: string | number | boolean | object | symbol | null | undefined; +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + +declare const s: symbol; +>s : Symbol(s, Decl(typeGuardWithRelaxEquality.ts, 16, 13)) + +declare const str: string; +>str : Symbol(str, Decl(typeGuardWithRelaxEquality.ts, 17, 13)) + +declare const num: number; +>num : Symbol(num, Decl(typeGuardWithRelaxEquality.ts, 18, 13)) + +declare const bool: boolean; +>bool : Symbol(bool, Decl(typeGuardWithRelaxEquality.ts, 19, 13)) + +if (a == 1) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 22, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == num) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +>num : Symbol(num, Decl(typeGuardWithRelaxEquality.ts, 18, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 25, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == '') { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 28, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == str) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +>str : Symbol(str, Decl(typeGuardWithRelaxEquality.ts, 17, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 31, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == false) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 34, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == bool) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +>bool : Symbol(bool, Decl(typeGuardWithRelaxEquality.ts, 19, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 37, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == {}) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 40, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == s) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +>s : Symbol(s, Decl(typeGuardWithRelaxEquality.ts, 16, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 43, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == null) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 46, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} +if (a == undefined) { +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +>undefined : Symbol(undefined) + + const t = a +>t : Symbol(t, Decl(typeGuardWithRelaxEquality.ts, 49, 9)) +>a : Symbol(a, Decl(typeGuardWithRelaxEquality.ts, 15, 13)) +} diff --git a/tests/baselines/reference/typeGuardWithRelaxEquality.types b/tests/baselines/reference/typeGuardWithRelaxEquality.types new file mode 100644 index 0000000000000..6b1786cf0b665 --- /dev/null +++ b/tests/baselines/reference/typeGuardWithRelaxEquality.types @@ -0,0 +1,161 @@ +=== tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts === +// Github issue #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 = level; // number | string +>q2 : string | number +>level : string | number + + return level; +>level : string | number + } + if (level === +level + 1) { +>level === +level + 1 : boolean +>level : string | number +>+level + 1 : number +>+level : number +>level : string | number +>1 : 1 + + const q2 = level; +>q2 : number +>level : number + + return level; +>level : number + } + return 0; +>0 : 0 +} +alert(test(5) + 1); +>alert(test(5) + 1) : void +>alert : (message?: any) => void +>test(5) + 1 : number +>test(5) : number +>test : (level: string | number) => number +>5 : 5 +>1 : 1 + +alert(test("5") + 1) +>alert(test("5") + 1) : void +>alert : (message?: any) => void +>test("5") + 1 : number +>test("5") : number +>test : (level: string | number) => number +>"5" : "5" +>1 : 1 + +declare const a: string | number | boolean | object | symbol | null | undefined; +>a : string | number | boolean | symbol | object | null | undefined +>null : null + +declare const s: symbol; +>s : symbol + +declare const str: string; +>str : string + +declare const num: number; +>num : number + +declare const bool: boolean; +>bool : boolean + +if (a == 1) { +>a == 1 : boolean +>a : string | number | boolean | symbol | object | null | undefined +>1 : 1 + + const t = a +>t : boolean | 1 +>a : boolean | 1 +} +if (a == num) { +>a == num : boolean +>a : string | number | boolean | symbol | object | null | undefined +>num : number + + const t = a +>t : string | number | boolean +>a : string | number | boolean +} +if (a == '') { +>a == '' : boolean +>a : string | number | boolean | symbol | object | null | undefined +>'' : "" + + const t = a +>t : boolean | "" +>a : boolean | "" +} +if (a == str) { +>a == str : boolean +>a : string | number | boolean | symbol | object | null | undefined +>str : string + + const t = a +>t : string | number | boolean +>a : string | number | boolean +} +if (a == false) { +>a == false : boolean +>a : string | number | boolean | symbol | object | null | undefined +>false : false + + const t = a +>t : string | number | false +>a : string | number | false +} +if (a == bool) { +>a == bool : boolean +>a : string | number | boolean | symbol | object | null | undefined +>bool : boolean + + const t = a +>t : string | number | boolean +>a : string | number | boolean +} +if (a == {}) { +>a == {} : boolean +>a : string | number | boolean | symbol | object | null | undefined +>{} : {} + + const t = a +>t : object +>a : object +} +if (a == s) { +>a == s : boolean +>a : string | number | boolean | symbol | object | null | undefined +>s : symbol + + const t = a +>t : symbol +>a : symbol +} +if (a == null) { +>a == null : boolean +>a : string | number | boolean | symbol | object | null | undefined +>null : null + + const t = a +>t : null | undefined +>a : null | undefined +} +if (a == undefined) { +>a == undefined : boolean +>a : string | number | boolean | symbol | object | null | undefined +>undefined : undefined + + const t = a +>t : null | undefined +>a : null | undefined +} diff --git a/tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts b/tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts new file mode 100644 index 0000000000000..d8bf4e6d519e2 --- /dev/null +++ b/tests/cases/conformance/expressions/typeGuards/typeGuardWithRelaxEquality.ts @@ -0,0 +1,53 @@ +// @strict: true + +// Github issue #24991 +function test(level: number | string):number { + if (level == +level) { + const q2 = level; // number | string + return level; + } + if (level === +level + 1) { + const q2 = level; + return level; + } + return 0; +} +alert(test(5) + 1); +alert(test("5") + 1) + +declare const a: string | number | boolean | object | symbol | null | undefined; +declare const s: symbol; +declare const str: string; +declare const num: number; +declare const bool: boolean; + +if (a == 1) { + const t = a +} +if (a == num) { + const t = a +} +if (a == '') { + const t = a +} +if (a == str) { + const t = a +} +if (a == false) { + const t = a +} +if (a == bool) { + const t = a +} +if (a == {}) { + const t = a +} +if (a == s) { + const t = a +} +if (a == null) { + const t = a +} +if (a == undefined) { + const t = a +} \ No newline at end of file