diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b345915b1f88e..304292896d5a2 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -195,7 +195,7 @@ namespace ts { None = 0, Source = 1 << 0, Target = 1 << 1, - ExcessCheck = 1 << 2, + PropertyCheck = 1 << 2, } const enum MappedTypeModifiers { @@ -15559,7 +15559,7 @@ namespace ts { if (source.flags & TypeFlags.Union) { result = relation === comparableRelation ? someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) : - eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & IntersectionState.ExcessCheck); + eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState); } else { if (target.flags & TypeFlags.Union) { @@ -15567,12 +15567,6 @@ namespace ts { } else if (target.flags & TypeFlags.Intersection) { result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target); - if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) && !(intersectionState & IntersectionState.ExcessCheck)) { - // Validate against excess props using the original `source` - if (!propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.ExcessCheck)) { - return Ternary.False; - } - } } else if (source.flags & TypeFlags.Intersection) { // Check to see if any constituents of the intersection are immediately related to the target. @@ -15588,9 +15582,7 @@ namespace ts { // // - For a primitive type or type parameter (such as 'number = A & B') there is no point in // breaking the intersection apart. - if (!isNonGenericObjectType(target) || !every((source).types, t => isNonGenericObjectType(t) && !(getObjectFlags(t) & ObjectFlags.NonInferrableType))) { - result = someTypeRelatedToType(source, target, /*reportErrors*/ false, IntersectionState.Source); - } + result = someTypeRelatedToType(source, target, /*reportErrors*/ false, IntersectionState.Source); } if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) { if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) { @@ -15622,6 +15614,23 @@ namespace ts { } } } + // For certain combinations involving intersections and optional, excess, or mismatched properties we need + // an extra property check where the intersection is viewed as a single object. The following are motivating + // examples that all should be errors, but aren't without this extra property check: + // + // let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property + // + // declare let wrong: { a: { y: string } }; + // let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type + // + // function foo(x: { a?: string }, y: T & { a: boolean }) { + // x = y; // Mismatched property in source intersection + // } + if (result && ( + target.flags & TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) || + isNonGenericObjectType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) { + result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck); + } if (!result && reportErrors) { source = originalSource.aliasSymbol ? originalSource : source; @@ -15998,6 +16007,9 @@ namespace ts { } function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary { + if (intersectionState & IntersectionState.PropertyCheck) { + return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); + } const flags = source.flags & target.flags; if (relation === identityRelation && !(flags & TypeFlags.Object)) { if (flags & TypeFlags.Index) { diff --git a/tests/baselines/reference/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.errors.txt b/tests/baselines/reference/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.errors.txt index 9abaa6db80393..af63c18523d6a 100644 --- a/tests/baselines/reference/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.errors.txt +++ b/tests/baselines/reference/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.errors.txt @@ -5,23 +5,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"] & ChannelOfType["type"]'. Type '"text"' is not assignable to type 'ChannelOfType["type"] & ChannelOfType["type"]'. Type '"text"' is not assignable to type 'ChannelOfType["type"]'. - Type '"text"' is not assignable to type 'T & "text"'. - Type '"text"' is not assignable to type 'T'. - '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. - Type 'T' is not assignable to type 'ChannelOfType["type"]'. - Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"]'. - Type '"text"' is not assignable to type 'ChannelOfType["type"]'. - Type '"text"' is not assignable to type 'T & "text"'. - Type '"text"' is not assignable to type 'T'. - '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. - Type 'T' is not assignable to type 'T & "text"'. - Type '"text" | "email"' is not assignable to type 'T & "text"'. - Type '"text"' is not assignable to type 'T & "text"'. - Type '"text"' is not assignable to type 'T'. - '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. - Type 'T' is not assignable to type '"text"'. - Type '"text" | "email"' is not assignable to type '"text"'. - Type '"email"' is not assignable to type '"text"'. + Type 'T' is not assignable to type 'ChannelOfType["type"]'. + Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"]'. + Type '"text"' is not assignable to type 'ChannelOfType["type"]'. + Type '"text"' is not assignable to type 'T & "text"'. + Type '"text"' is not assignable to type 'T'. + '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. + Type 'T' is not assignable to type 'T & "text"'. + Type '"text" | "email"' is not assignable to type 'T & "text"'. + Type '"text"' is not assignable to type 'T & "text"'. + Type '"text"' is not assignable to type 'T'. + '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. + Type 'T' is not assignable to type '"text"'. + Type '"text" | "email"' is not assignable to type '"text"'. + Type '"email"' is not assignable to type '"text"'. ==== tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.ts (1 errors) ==== @@ -66,23 +63,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t !!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"] & ChannelOfType["type"]'. !!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType["type"] & ChannelOfType["type"]'. !!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType["type"]'. -!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'. -!!! error TS2322: Type '"text"' is not assignable to type 'T'. -!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. -!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType["type"]'. -!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"]'. -!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType["type"]'. -!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'. -!!! error TS2322: Type '"text"' is not assignable to type 'T'. -!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. -!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'. -!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'. -!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'. -!!! error TS2322: Type '"text"' is not assignable to type 'T'. -!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. -!!! error TS2322: Type 'T' is not assignable to type '"text"'. -!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'. -!!! error TS2322: Type '"email"' is not assignable to type '"text"'. +!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType["type"]'. +!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType["type"]'. +!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType["type"]'. +!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'. +!!! error TS2322: Type '"text"' is not assignable to type 'T'. +!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. +!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'. +!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'. +!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'. +!!! error TS2322: Type '"text"' is not assignable to type 'T'. +!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'. +!!! error TS2322: Type 'T' is not assignable to type '"text"'. +!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'. +!!! error TS2322: Type '"email"' is not assignable to type '"text"'. } const newTextChannel = makeNewChannel('text'); diff --git a/tests/baselines/reference/intersectionPropertyCheck.errors.txt b/tests/baselines/reference/intersectionPropertyCheck.errors.txt new file mode 100644 index 0000000000000..7c74c3512eaf1 --- /dev/null +++ b/tests/baselines/reference/intersectionPropertyCheck.errors.txt @@ -0,0 +1,46 @@ +tests/cases/compiler/intersectionPropertyCheck.ts(1,68): error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'. + Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'. +tests/cases/compiler/intersectionPropertyCheck.ts(4,5): error TS2322: Type '{ a: { y: string; }; }' is not assignable to type '{ a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }'. + Types of property 'a' are incompatible. + Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'. +tests/cases/compiler/intersectionPropertyCheck.ts(7,3): error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'. + Types of property 'a' are incompatible. + Type 'boolean' is not assignable to type 'string | undefined'. +tests/cases/compiler/intersectionPropertyCheck.ts(17,22): error TS2322: Type 'true' is not assignable to type 'string[] | undefined'. + + +==== tests/cases/compiler/intersectionPropertyCheck.ts (4 errors) ==== + let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property + ~~~~ +!!! error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'. +!!! error TS2322: Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'. +!!! related TS6500 tests/cases/compiler/intersectionPropertyCheck.ts:1:12: The expected type comes from property 'a' which is declared here on type '{ a: { x: string; }; } & { c: number; }' + + declare let wrong: { a: { y: string } }; + let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type + ~~~~ +!!! error TS2322: Type '{ a: { y: string; }; }' is not assignable to type '{ a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }'. +!!! error TS2322: Types of property 'a' are incompatible. +!!! error TS2322: Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'. + + function foo(x: { a?: string }, y: T & { a: boolean }) { + x = y; // Mismatched property in source intersection + ~ +!!! error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'. +!!! error TS2322: Types of property 'a' are incompatible. +!!! error TS2322: Type 'boolean' is not assignable to type 'string | undefined'. + } + + // Repro from #36637 + + interface Test { + readonly hi?: string[] + } + + function test(value: T): Test { + return { ...value, hi: true } + ~~ +!!! error TS2322: Type 'true' is not assignable to type 'string[] | undefined'. +!!! related TS6500 tests/cases/compiler/intersectionPropertyCheck.ts:13:12: The expected type comes from property 'hi' which is declared here on type 'Test' + } + \ No newline at end of file diff --git a/tests/baselines/reference/intersectionPropertyCheck.js b/tests/baselines/reference/intersectionPropertyCheck.js new file mode 100644 index 0000000000000..a62491b83667a --- /dev/null +++ b/tests/baselines/reference/intersectionPropertyCheck.js @@ -0,0 +1,42 @@ +//// [intersectionPropertyCheck.ts] +let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property + +declare let wrong: { a: { y: string } }; +let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type + +function foo(x: { a?: string }, y: T & { a: boolean }) { + x = y; // Mismatched property in source intersection +} + +// Repro from #36637 + +interface Test { + readonly hi?: string[] +} + +function test(value: T): Test { + return { ...value, hi: true } +} + + +//// [intersectionPropertyCheck.js] +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var obj = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property +var weak = wrong; // Nested weak object type +function foo(x, y) { + x = y; // Mismatched property in source intersection +} +function test(value) { + return __assign(__assign({}, value), { hi: true }); +} diff --git a/tests/baselines/reference/intersectionPropertyCheck.symbols b/tests/baselines/reference/intersectionPropertyCheck.symbols new file mode 100644 index 0000000000000..e1fec18bf6938 --- /dev/null +++ b/tests/baselines/reference/intersectionPropertyCheck.symbols @@ -0,0 +1,58 @@ +=== tests/cases/compiler/intersectionPropertyCheck.ts === +let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property +>obj : Symbol(obj, Decl(intersectionPropertyCheck.ts, 0, 3)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 0, 10)) +>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 0, 15)) +>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 0, 33)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 0, 49)) +>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 0, 54)) +>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 0, 66)) +>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 0, 74)) + +declare let wrong: { a: { y: string } }; +>wrong : Symbol(wrong, Decl(intersectionPropertyCheck.ts, 2, 11)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 2, 20)) +>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 2, 25)) + +let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type +>weak : Symbol(weak, Decl(intersectionPropertyCheck.ts, 3, 3)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 3, 11)) +>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 3, 17)) +>c : Symbol(c, Decl(intersectionPropertyCheck.ts, 3, 36)) +>wrong : Symbol(wrong, Decl(intersectionPropertyCheck.ts, 2, 11)) + +function foo(x: { a?: string }, y: T & { a: boolean }) { +>foo : Symbol(foo, Decl(intersectionPropertyCheck.ts, 3, 58)) +>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 5, 13)) +>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 5, 31)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 5, 35)) +>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 5, 49)) +>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 5, 13)) +>a : Symbol(a, Decl(intersectionPropertyCheck.ts, 5, 58)) + + x = y; // Mismatched property in source intersection +>x : Symbol(x, Decl(intersectionPropertyCheck.ts, 5, 31)) +>y : Symbol(y, Decl(intersectionPropertyCheck.ts, 5, 49)) +} + +// Repro from #36637 + +interface Test { +>Test : Symbol(Test, Decl(intersectionPropertyCheck.ts, 7, 1)) + + readonly hi?: string[] +>hi : Symbol(Test.hi, Decl(intersectionPropertyCheck.ts, 11, 16)) +} + +function test(value: T): Test { +>test : Symbol(test, Decl(intersectionPropertyCheck.ts, 13, 1)) +>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 15, 14)) +>value : Symbol(value, Decl(intersectionPropertyCheck.ts, 15, 32)) +>T : Symbol(T, Decl(intersectionPropertyCheck.ts, 15, 14)) +>Test : Symbol(Test, Decl(intersectionPropertyCheck.ts, 7, 1)) + + return { ...value, hi: true } +>value : Symbol(value, Decl(intersectionPropertyCheck.ts, 15, 32)) +>hi : Symbol(hi, Decl(intersectionPropertyCheck.ts, 16, 20)) +} + diff --git a/tests/baselines/reference/intersectionPropertyCheck.types b/tests/baselines/reference/intersectionPropertyCheck.types new file mode 100644 index 0000000000000..515d5009732fd --- /dev/null +++ b/tests/baselines/reference/intersectionPropertyCheck.types @@ -0,0 +1,59 @@ +=== tests/cases/compiler/intersectionPropertyCheck.ts === +let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property +>obj : { a: { x: string;}; } & { c: number; } +>a : { x: string; } +>x : string +>c : number +>{ a: { x: 'hello', y: 2 }, c: 5 } : { a: { x: string; y: number; }; c: number; } +>a : { x: string; y: number; } +>{ x: 'hello', y: 2 } : { x: string; y: number; } +>x : string +>'hello' : "hello" +>y : number +>2 : 2 +>c : number +>5 : 5 + +declare let wrong: { a: { y: string } }; +>wrong : { a: { y: string;}; } +>a : { y: string; } +>y : string + +let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type +>weak : { a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; } +>a : { x?: number | undefined; } | undefined +>x : number | undefined +>c : string | undefined +>wrong : { a: { y: string; }; } + +function foo(x: { a?: string }, y: T & { a: boolean }) { +>foo : (x: { a?: string;}, y: T & { a: boolean;}) => void +>x : { a?: string | undefined; } +>a : string | undefined +>y : T & { a: boolean; } +>a : boolean + + x = y; // Mismatched property in source intersection +>x = y : T & { a: boolean; } +>x : { a?: string | undefined; } +>y : T & { a: boolean; } +} + +// Repro from #36637 + +interface Test { + readonly hi?: string[] +>hi : string[] | undefined +} + +function test(value: T): Test { +>test : (value: T) => Test +>value : T + + return { ...value, hi: true } +>{ ...value, hi: true } : T & { hi: boolean; } +>value : T +>hi : boolean +>true : true +} + diff --git a/tests/baselines/reference/weakType.errors.txt b/tests/baselines/reference/weakType.errors.txt index 9fc9deab703c3..477087a585992 100644 --- a/tests/baselines/reference/weakType.errors.txt +++ b/tests/baselines/reference/weakType.errors.txt @@ -5,8 +5,9 @@ tests/cases/compiler/weakType.ts(18,13): error TS2559: Type '12' has no properti tests/cases/compiler/weakType.ts(19,13): error TS2559: Type '"completely wrong"' has no properties in common with type 'Settings'. tests/cases/compiler/weakType.ts(20,13): error TS2559: Type 'false' has no properties in common with type 'Settings'. tests/cases/compiler/weakType.ts(37,18): error TS2559: Type '{ error?: number; }' has no properties in common with type 'ChangeOptions'. -tests/cases/compiler/weakType.ts(62,5): error TS2326: Types of property 'properties' are incompatible. - Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'. +tests/cases/compiler/weakType.ts(62,5): error TS2322: Type '{ properties: { wrong: string; }; }' is not assignable to type 'Weak & Spoiler'. + Types of property 'properties' are incompatible. + Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'. ==== tests/cases/compiler/weakType.ts (8 errors) ==== @@ -90,7 +91,8 @@ tests/cases/compiler/weakType.ts(62,5): error TS2326: Types of property 'propert } let weak: Weak & Spoiler = propertiesWrong ~~~~ -!!! error TS2326: Types of property 'properties' are incompatible. -!!! error TS2326: Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'. +!!! error TS2322: Type '{ properties: { wrong: string; }; }' is not assignable to type 'Weak & Spoiler'. +!!! error TS2322: Types of property 'properties' are incompatible. +!!! error TS2322: Type '{ wrong: string; }' has no properties in common with type '{ b?: number; }'. \ No newline at end of file diff --git a/tests/cases/compiler/intersectionPropertyCheck.ts b/tests/cases/compiler/intersectionPropertyCheck.ts new file mode 100644 index 0000000000000..77bfd31097ec6 --- /dev/null +++ b/tests/cases/compiler/intersectionPropertyCheck.ts @@ -0,0 +1,20 @@ +// @strict: true + +let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property + +declare let wrong: { a: { y: string } }; +let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type + +function foo(x: { a?: string }, y: T & { a: boolean }) { + x = y; // Mismatched property in source intersection +} + +// Repro from #36637 + +interface Test { + readonly hi?: string[] +} + +function test(value: T): Test { + return { ...value, hi: true } +}