Skip to content
Merged
34 changes: 23 additions & 11 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ namespace ts {
None = 0,
Source = 1 << 0,
Target = 1 << 1,
ExcessCheck = 1 << 2,
PropertyCheck = 1 << 2,
}

const enum MappedTypeModifiers {
Expand Down Expand Up @@ -15559,20 +15559,14 @@ 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) {
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
}
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.
Expand All @@ -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((<IntersectionType>source).types, t => isNonGenericObjectType(t) && !(getObjectFlags(t) & ObjectFlags.NonInferrableType))) {
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
}
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
}
if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) {
if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) {
Expand Down Expand Up @@ -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<T extends object>(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((<IntersectionType>source).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck);
}

if (!result && reportErrors) {
source = originalSource.aliasSymbol ? originalSource : source;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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<T, TextChannel>["type"]'.
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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<T, TextChannel>["type"]'.
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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) ====
Expand Down Expand Up @@ -66,23 +63,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["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');
Expand Down
46 changes: 46 additions & 0 deletions tests/baselines/reference/intersectionPropertyCheck.errors.txt
Original file line number Diff line number Diff line change
@@ -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<T extends object>(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<T extends object>(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'
}

42 changes: 42 additions & 0 deletions tests/baselines/reference/intersectionPropertyCheck.js
Original file line number Diff line number Diff line change
@@ -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<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
x = y; // Mismatched property in source intersection
}

// Repro from #36637

interface Test {
readonly hi?: string[]
}

function test<T extends object>(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 });
}
Loading