Skip to content

Commit a2609b1

Browse files
authored
Extra check in assignment of intersections with generic constituents (#37537)
* Consolidated extra property check with intersections * Fix comment * Add tests * Properly propagate intersectionState * Route property check through recursive type tracking logic * Accept new baselines * Skip check when apparent type of source is never * Accept new baselines * Only check when apparent type of source is a structured type
1 parent 5a4024d commit a2609b1

8 files changed

+282
-49
lines changed

src/compiler/checker.ts

+23-11
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ namespace ts {
195195
None = 0,
196196
Source = 1 << 0,
197197
Target = 1 << 1,
198-
ExcessCheck = 1 << 2,
198+
PropertyCheck = 1 << 2,
199199
}
200200

201201
const enum MappedTypeModifiers {
@@ -15561,20 +15561,14 @@ namespace ts {
1556115561
if (source.flags & TypeFlags.Union) {
1556215562
result = relation === comparableRelation ?
1556315563
someTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState) :
15564-
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState & IntersectionState.ExcessCheck);
15564+
eachTypeRelatedToType(source as UnionType, target, reportErrors && !(source.flags & TypeFlags.Primitive), intersectionState);
1556515565
}
1556615566
else {
1556715567
if (target.flags & TypeFlags.Union) {
1556815568
result = typeRelatedToSomeType(getRegularTypeOfObjectLiteral(source), <UnionType>target, reportErrors && !(source.flags & TypeFlags.Primitive) && !(target.flags & TypeFlags.Primitive));
1556915569
}
1557015570
else if (target.flags & TypeFlags.Intersection) {
1557115571
result = typeRelatedToEachType(getRegularTypeOfObjectLiteral(source), target as IntersectionType, reportErrors, IntersectionState.Target);
15572-
if (result && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) && !(intersectionState & IntersectionState.ExcessCheck)) {
15573-
// Validate against excess props using the original `source`
15574-
if (!propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.ExcessCheck)) {
15575-
return Ternary.False;
15576-
}
15577-
}
1557815572
}
1557915573
else if (source.flags & TypeFlags.Intersection) {
1558015574
// Check to see if any constituents of the intersection are immediately related to the target.
@@ -15590,9 +15584,7 @@ namespace ts {
1559015584
//
1559115585
// - For a primitive type or type parameter (such as 'number = A & B') there is no point in
1559215586
// breaking the intersection apart.
15593-
if (!isNonGenericObjectType(target) || !every((<IntersectionType>source).types, t => isNonGenericObjectType(t) && !(getObjectFlags(t) & ObjectFlags.NonInferrableType))) {
15594-
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
15595-
}
15587+
result = someTypeRelatedToType(<IntersectionType>source, target, /*reportErrors*/ false, IntersectionState.Source);
1559615588
}
1559715589
if (!result && (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable)) {
1559815590
if (result = recursiveTypeRelatedTo(source, target, reportErrors, intersectionState)) {
@@ -15624,6 +15616,23 @@ namespace ts {
1562415616
}
1562515617
}
1562615618
}
15619+
// For certain combinations involving intersections and optional, excess, or mismatched properties we need
15620+
// an extra property check where the intersection is viewed as a single object. The following are motivating
15621+
// examples that all should be errors, but aren't without this extra property check:
15622+
//
15623+
// let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
15624+
//
15625+
// declare let wrong: { a: { y: string } };
15626+
// let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
15627+
//
15628+
// function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
15629+
// x = y; // Mismatched property in source intersection
15630+
// }
15631+
if (result && (
15632+
target.flags & TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) ||
15633+
isNonGenericObjectType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((<IntersectionType>source).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
15634+
result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck);
15635+
}
1562715636

1562815637
if (!result && reportErrors) {
1562915638
source = originalSource.aliasSymbol ? originalSource : source;
@@ -16000,6 +16009,9 @@ namespace ts {
1600016009
}
1600116010

1600216011
function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
16012+
if (intersectionState & IntersectionState.PropertyCheck) {
16013+
return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
16014+
}
1600316015
const flags = source.flags & target.flags;
1600416016
if (relation === identityRelation && !(flags & TypeFlags.Object)) {
1600516017
if (flags & TypeFlags.Index) {

tests/baselines/reference/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.errors.txt

+28-34
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
55
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
66
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
77
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
8-
Type '"text"' is not assignable to type 'T & "text"'.
9-
Type '"text"' is not assignable to type 'T'.
10-
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
11-
Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
12-
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
13-
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
14-
Type '"text"' is not assignable to type 'T & "text"'.
15-
Type '"text"' is not assignable to type 'T'.
16-
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
17-
Type 'T' is not assignable to type 'T & "text"'.
18-
Type '"text" | "email"' is not assignable to type 'T & "text"'.
19-
Type '"text"' is not assignable to type 'T & "text"'.
20-
Type '"text"' is not assignable to type 'T'.
21-
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
22-
Type 'T' is not assignable to type '"text"'.
23-
Type '"text" | "email"' is not assignable to type '"text"'.
24-
Type '"email"' is not assignable to type '"text"'.
8+
Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
9+
Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
10+
Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
11+
Type '"text"' is not assignable to type 'T & "text"'.
12+
Type '"text"' is not assignable to type 'T'.
13+
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
14+
Type 'T' is not assignable to type 'T & "text"'.
15+
Type '"text" | "email"' is not assignable to type 'T & "text"'.
16+
Type '"text"' is not assignable to type 'T & "text"'.
17+
Type '"text"' is not assignable to type 'T'.
18+
'"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
19+
Type 'T' is not assignable to type '"text"'.
20+
Type '"text" | "email"' is not assignable to type '"text"'.
21+
Type '"email"' is not assignable to type '"text"'.
2522

2623

2724
==== tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.ts (1 errors) ====
@@ -66,23 +63,20 @@ tests/cases/compiler/complicatedIndexedAccessKeyofReliesOnKeyofNeverUpperBound.t
6663
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
6764
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"] & ChannelOfType<T, EmailChannel>["type"]'.
6865
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
69-
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
70-
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
71-
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
72-
!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
73-
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
74-
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
75-
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
76-
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
77-
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
78-
!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'.
79-
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'.
80-
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
81-
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
82-
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
83-
!!! error TS2322: Type 'T' is not assignable to type '"text"'.
84-
!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'.
85-
!!! error TS2322: Type '"email"' is not assignable to type '"text"'.
66+
!!! error TS2322: Type 'T' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
67+
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
68+
!!! error TS2322: Type '"text"' is not assignable to type 'ChannelOfType<T, TextChannel>["type"]'.
69+
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
70+
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
71+
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
72+
!!! error TS2322: Type 'T' is not assignable to type 'T & "text"'.
73+
!!! error TS2322: Type '"text" | "email"' is not assignable to type 'T & "text"'.
74+
!!! error TS2322: Type '"text"' is not assignable to type 'T & "text"'.
75+
!!! error TS2322: Type '"text"' is not assignable to type 'T'.
76+
!!! error TS2322: '"text"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"text" | "email"'.
77+
!!! error TS2322: Type 'T' is not assignable to type '"text"'.
78+
!!! error TS2322: Type '"text" | "email"' is not assignable to type '"text"'.
79+
!!! error TS2322: Type '"email"' is not assignable to type '"text"'.
8680
}
8781

8882
const newTextChannel = makeNewChannel('text');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
tests/cases/compiler/intersectionPropertyCheck.ts(1,68): error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'.
2+
Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'.
3+
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; }'.
4+
Types of property 'a' are incompatible.
5+
Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'.
6+
tests/cases/compiler/intersectionPropertyCheck.ts(7,3): error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'.
7+
Types of property 'a' are incompatible.
8+
Type 'boolean' is not assignable to type 'string | undefined'.
9+
tests/cases/compiler/intersectionPropertyCheck.ts(17,22): error TS2322: Type 'true' is not assignable to type 'string[] | undefined'.
10+
11+
12+
==== tests/cases/compiler/intersectionPropertyCheck.ts (4 errors) ====
13+
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
14+
~~~~
15+
!!! error TS2322: Type '{ x: string; y: number; }' is not assignable to type '{ x: string; }'.
16+
!!! error TS2322: Object literal may only specify known properties, and 'y' does not exist in type '{ x: string; }'.
17+
!!! 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; }'
18+
19+
declare let wrong: { a: { y: string } };
20+
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
21+
~~~~
22+
!!! error TS2322: Type '{ a: { y: string; }; }' is not assignable to type '{ a?: { x?: number | undefined; } | undefined; } & { c?: string | undefined; }'.
23+
!!! error TS2322: Types of property 'a' are incompatible.
24+
!!! error TS2322: Type '{ y: string; }' has no properties in common with type '{ x?: number | undefined; }'.
25+
26+
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
27+
x = y; // Mismatched property in source intersection
28+
~
29+
!!! error TS2322: Type 'T & { a: boolean; }' is not assignable to type '{ a?: string | undefined; }'.
30+
!!! error TS2322: Types of property 'a' are incompatible.
31+
!!! error TS2322: Type 'boolean' is not assignable to type 'string | undefined'.
32+
}
33+
34+
// Repro from #36637
35+
36+
interface Test {
37+
readonly hi?: string[]
38+
}
39+
40+
function test<T extends object>(value: T): Test {
41+
return { ...value, hi: true }
42+
~~
43+
!!! error TS2322: Type 'true' is not assignable to type 'string[] | undefined'.
44+
!!! related TS6500 tests/cases/compiler/intersectionPropertyCheck.ts:13:12: The expected type comes from property 'hi' which is declared here on type 'Test'
45+
}
46+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//// [intersectionPropertyCheck.ts]
2+
let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
3+
4+
declare let wrong: { a: { y: string } };
5+
let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
6+
7+
function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
8+
x = y; // Mismatched property in source intersection
9+
}
10+
11+
// Repro from #36637
12+
13+
interface Test {
14+
readonly hi?: string[]
15+
}
16+
17+
function test<T extends object>(value: T): Test {
18+
return { ...value, hi: true }
19+
}
20+
21+
22+
//// [intersectionPropertyCheck.js]
23+
"use strict";
24+
var __assign = (this && this.__assign) || function () {
25+
__assign = Object.assign || function(t) {
26+
for (var s, i = 1, n = arguments.length; i < n; i++) {
27+
s = arguments[i];
28+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
29+
t[p] = s[p];
30+
}
31+
return t;
32+
};
33+
return __assign.apply(this, arguments);
34+
};
35+
var obj = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
36+
var weak = wrong; // Nested weak object type
37+
function foo(x, y) {
38+
x = y; // Mismatched property in source intersection
39+
}
40+
function test(value) {
41+
return __assign(__assign({}, value), { hi: true });
42+
}

0 commit comments

Comments
 (0)