Skip to content

Commit 37317a2

Browse files
authoredOct 13, 2022
Check nested weak types in intersections on target side of relation (#51140)
* Check nested weak types in intersections on target side of relation * Add regression tests * Move logic from isRelatedTo to structuredTypeRelatedTo * Fix lint error * Add additional test
1 parent 9f49f9c commit 37317a2

6 files changed

+431
-37
lines changed
 

‎src/compiler/checker.ts

+33-37
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,6 @@ namespace ts {
194194
None = 0,
195195
Source = 1 << 0,
196196
Target = 1 << 1,
197-
PropertyCheck = 1 << 2,
198-
InPropertyCheck = 1 << 3,
199197
}
200198

201199
const enum RecursionFlags {
@@ -19112,7 +19110,7 @@ namespace ts {
1911219110
}
1911319111
}
1911419112

19115-
const isPerformingCommonPropertyChecks = (relation !== comparableRelation || !(source.flags & TypeFlags.Union) && isLiteralType(source)) &&
19113+
const isPerformingCommonPropertyChecks = (relation !== comparableRelation || isUnitType(source)) &&
1911619114
!(intersectionState & IntersectionState.Target) &&
1911719115
source.flags & (TypeFlags.Primitive | TypeFlags.Object | TypeFlags.Intersection) && source !== globalObjectType &&
1911819116
target.flags & (TypeFlags.Object | TypeFlags.Intersection) && isWeakType(target) &&
@@ -19139,31 +19137,9 @@ namespace ts {
1913919137

1914019138
const skipCaching = source.flags & TypeFlags.Union && (source as UnionType).types.length < 4 && !(target.flags & TypeFlags.Union) ||
1914119139
target.flags & TypeFlags.Union && (target as UnionType).types.length < 4 && !(source.flags & TypeFlags.StructuredOrInstantiable);
19142-
let result = skipCaching ?
19140+
const result = skipCaching ?
1914319141
unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState) :
1914419142
recursiveTypeRelatedTo(source, target, reportErrors, intersectionState, recursionFlags);
19145-
// For certain combinations involving intersections and optional, excess, or mismatched properties we need
19146-
// an extra property check where the intersection is viewed as a single object. The following are motivating
19147-
// examples that all should be errors, but aren't without this extra property check:
19148-
//
19149-
// let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
19150-
//
19151-
// declare let wrong: { a: { y: string } };
19152-
// let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
19153-
//
19154-
// function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
19155-
// x = y; // Mismatched property in source intersection
19156-
// }
19157-
//
19158-
// We suppress recursive intersection property checks because they can generate lots of work when relating
19159-
// recursive intersections that are structurally similar but not exactly identical. See #37854.
19160-
if (result && !inPropertyCheck && (
19161-
target.flags & TypeFlags.Intersection && (isPerformingExcessPropertyChecks || isPerformingCommonPropertyChecks) ||
19162-
isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
19163-
inPropertyCheck = true;
19164-
result &= recursiveTypeRelatedTo(source, target, reportErrors, IntersectionState.PropertyCheck, recursionFlags);
19165-
inPropertyCheck = false;
19166-
}
1916719143
if (result) {
1916819144
return result;
1916919145
}
@@ -19567,8 +19543,7 @@ namespace ts {
1956719543
if (overflow) {
1956819544
return Ternary.False;
1956919545
}
19570-
const keyIntersectionState = intersectionState | (inPropertyCheck ? IntersectionState.InPropertyCheck : 0);
19571-
const id = getRelationKey(source, target, keyIntersectionState, relation, /*ingnoreConstraints*/ false);
19546+
const id = getRelationKey(source, target, intersectionState, relation, /*ingnoreConstraints*/ false);
1957219547
const entry = relation.get(id);
1957319548
if (entry !== undefined) {
1957419549
if (reportErrors && entry & RelationComparisonResult.Failed && !(entry & RelationComparisonResult.Reported)) {
@@ -19598,7 +19573,7 @@ namespace ts {
1959819573
// A key that starts with "*" is an indication that we have type references that reference constrained
1959919574
// type parameters. For such keys we also check against the key we would have gotten if all type parameters
1960019575
// were unconstrained.
19601-
const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, keyIntersectionState, relation, /*ignoreConstraints*/ true) : undefined;
19576+
const broadestEquivalentId = id.startsWith("*") ? getRelationKey(source, target, intersectionState, relation, /*ignoreConstraints*/ true) : undefined;
1960219577
for (let i = 0; i < maybeCount; i++) {
1960319578
// If source and target are already being compared, consider them related with assumptions
1960419579
if (id === maybeKeys[i] || broadestEquivalentId && broadestEquivalentId === maybeKeys[i]) {
@@ -19686,7 +19661,7 @@ namespace ts {
1968619661
function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState): Ternary {
1968719662
const saveErrorInfo = captureErrorCalculationState();
1968819663
let result = structuredTypeRelatedToWorker(source, target, reportErrors, intersectionState, saveErrorInfo);
19689-
if (!result && (source.flags & TypeFlags.Intersection || source.flags & TypeFlags.TypeParameter && target.flags & TypeFlags.Union)) {
19664+
if (relation !== identityRelation) {
1969019665
// The combined constraint of an intersection type is the intersection of the constraints of
1969119666
// the constituents. When an intersection type contains instantiable types with union type
1969219667
// constraints, there are situations where we need to examine the combined constraint. One is
@@ -19700,10 +19675,34 @@ namespace ts {
1970019675
// needs to have its constraint hoisted into an intersection with said type parameter, this way
1970119676
// the type param can be compared with itself in the target (with the influence of its constraint to match other parts)
1970219677
// For example, if `T extends 1 | 2` and `U extends 2 | 3` and we compare `T & U` to `T & U & (1 | 2 | 3)`
19703-
const constraint = getEffectiveConstraintOfIntersection(source.flags & TypeFlags.Intersection ? (source as IntersectionType).types: [source], !!(target.flags & TypeFlags.Union));
19704-
if (constraint && !(constraint.flags & TypeFlags.Never) && everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself
19705-
// TODO: Stack errors so we get a pyramid for the "normal" comparison above, _and_ a second for this
19706-
result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState);
19678+
if (!result && (source.flags & TypeFlags.Intersection || source.flags & TypeFlags.TypeParameter && target.flags & TypeFlags.Union)) {
19679+
const constraint = getEffectiveConstraintOfIntersection(source.flags & TypeFlags.Intersection ? (source as IntersectionType).types: [source], !!(target.flags & TypeFlags.Union));
19680+
if (constraint && !(constraint.flags & TypeFlags.Never) && everyType(constraint, c => c !== source)) { // Skip comparison if expansion contains the source itself
19681+
// TODO: Stack errors so we get a pyramid for the "normal" comparison above, _and_ a second for this
19682+
result = isRelatedTo(constraint, target, RecursionFlags.Source, /*reportErrors*/ false, /*headMessage*/ undefined, intersectionState);
19683+
}
19684+
}
19685+
// For certain combinations involving intersections and optional, excess, or mismatched properties we need
19686+
// an extra property check where the intersection is viewed as a single object. The following are motivating
19687+
// examples that all should be errors, but aren't without this extra property check:
19688+
//
19689+
// let obj: { a: { x: string } } & { c: number } = { a: { x: 'hello', y: 2 }, c: 5 }; // Nested excess property
19690+
//
19691+
// declare let wrong: { a: { y: string } };
19692+
// let weak: { a?: { x?: number } } & { c?: string } = wrong; // Nested weak object type
19693+
//
19694+
// function foo<T extends object>(x: { a?: string }, y: T & { a: boolean }) {
19695+
// x = y; // Mismatched property in source intersection
19696+
// }
19697+
//
19698+
// We suppress recursive intersection property checks because they can generate lots of work when relating
19699+
// recursive intersections that are structurally similar but not exactly identical. See #37854.
19700+
if (result && !inPropertyCheck && (
19701+
target.flags & TypeFlags.Intersection && !isGenericObjectType(target) && source.flags & (TypeFlags.Object | TypeFlags.Intersection) ||
19702+
isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) {
19703+
inPropertyCheck = true;
19704+
result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
19705+
inPropertyCheck = false;
1970719706
}
1970819707
}
1970919708
if (result) {
@@ -19713,9 +19712,6 @@ namespace ts {
1971319712
}
1971419713

1971519714
function structuredTypeRelatedToWorker(source: Type, target: Type, reportErrors: boolean, intersectionState: IntersectionState, saveErrorInfo: ReturnType<typeof captureErrorCalculationState>): Ternary {
19716-
if (intersectionState & IntersectionState.PropertyCheck) {
19717-
return propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None);
19718-
}
1971919715
let result: Ternary;
1972019716
let originalErrorInfo: DiagnosticMessageChain | undefined;
1972119717
let varianceCheckFailed = false;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
tests/cases/compiler/nestedExcessPropertyChecking.ts(6,7): error TS2322: Type 'C1' is not assignable to type 'A1 & B1'.
2+
Types of property 'x' are incompatible.
3+
Type '{ c: string; }' has no properties in common with type '{ a?: string | undefined; } & { b?: string | undefined; }'.
4+
tests/cases/compiler/nestedExcessPropertyChecking.ts(13,7): error TS2559: Type 'C2' has no properties in common with type 'A2 & B2'.
5+
tests/cases/compiler/nestedExcessPropertyChecking.ts(17,5): error TS2559: Type 'E' has no properties in common with type '{ nope?: any; }'.
6+
tests/cases/compiler/nestedExcessPropertyChecking.ts(18,5): error TS2559: Type '"A"' has no properties in common with type '{ nope?: any; }'.
7+
tests/cases/compiler/nestedExcessPropertyChecking.ts(30,22): error TS2559: Type 'false' has no properties in common with type 'OverridesInput'.
8+
tests/cases/compiler/nestedExcessPropertyChecking.ts(40,9): error TS2559: Type 'false' has no properties in common with type 'OverridesInput'.
9+
10+
11+
==== tests/cases/compiler/nestedExcessPropertyChecking.ts (6 errors) ====
12+
type A1 = { x: { a?: string } };
13+
type B1 = { x: { b?: string } };
14+
15+
type C1 = { x: { c: string } };
16+
17+
const ab1: A1 & B1 = {} as C1; // Error
18+
~~~
19+
!!! error TS2322: Type 'C1' is not assignable to type 'A1 & B1'.
20+
!!! error TS2322: Types of property 'x' are incompatible.
21+
!!! error TS2322: Type '{ c: string; }' has no properties in common with type '{ a?: string | undefined; } & { b?: string | undefined; }'.
22+
23+
type A2 = { a?: string };
24+
type B2 = { b?: string };
25+
26+
type C2 = { c: string };
27+
28+
const ab2: A2 & B2 = {} as C2; // Error
29+
~~~
30+
!!! error TS2559: Type 'C2' has no properties in common with type 'A2 & B2'.
31+
32+
enum E { A = "A" }
33+
34+
let x: { nope?: any } = E.A; // Error
35+
~
36+
!!! error TS2559: Type 'E' has no properties in common with type '{ nope?: any; }'.
37+
let y: { nope?: any } = "A"; // Error
38+
~
39+
!!! error TS2559: Type '"A"' has no properties in common with type '{ nope?: any; }'.
40+
41+
// Repros from #51043
42+
43+
type OverridesInput = {
44+
someProp?: 'A' | 'B'
45+
}
46+
47+
const foo1: Partial<{ something: any }> & { variables: {
48+
overrides?: OverridesInput;
49+
} & Partial<{
50+
overrides?: OverridesInput;
51+
}>} = { variables: { overrides: false } }; // Error
52+
~~~~~~~~~
53+
!!! error TS2559: Type 'false' has no properties in common with type 'OverridesInput'.
54+
!!! related TS6500 tests/cases/compiler/nestedExcessPropertyChecking.ts:27:5: The expected type comes from property 'overrides' which is declared here on type '{ overrides?: OverridesInput | undefined; } & Partial<{ overrides?: OverridesInput | undefined; }>'
55+
56+
57+
interface Unrelated { _?: any }
58+
59+
interface VariablesA { overrides?: OverridesInput; }
60+
interface VariablesB { overrides?: OverridesInput; }
61+
62+
const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
63+
variables: {
64+
overrides: false // Error
65+
~~~~~~~~~
66+
!!! error TS2559: Type 'false' has no properties in common with type 'OverridesInput'.
67+
!!! related TS6500 tests/cases/compiler/nestedExcessPropertyChecking.ts:35:24: The expected type comes from property 'overrides' which is declared here on type 'VariablesA & VariablesB'
68+
}
69+
};
70+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//// [nestedExcessPropertyChecking.ts]
2+
type A1 = { x: { a?: string } };
3+
type B1 = { x: { b?: string } };
4+
5+
type C1 = { x: { c: string } };
6+
7+
const ab1: A1 & B1 = {} as C1; // Error
8+
9+
type A2 = { a?: string };
10+
type B2 = { b?: string };
11+
12+
type C2 = { c: string };
13+
14+
const ab2: A2 & B2 = {} as C2; // Error
15+
16+
enum E { A = "A" }
17+
18+
let x: { nope?: any } = E.A; // Error
19+
let y: { nope?: any } = "A"; // Error
20+
21+
// Repros from #51043
22+
23+
type OverridesInput = {
24+
someProp?: 'A' | 'B'
25+
}
26+
27+
const foo1: Partial<{ something: any }> & { variables: {
28+
overrides?: OverridesInput;
29+
} & Partial<{
30+
overrides?: OverridesInput;
31+
}>} = { variables: { overrides: false } }; // Error
32+
33+
34+
interface Unrelated { _?: any }
35+
36+
interface VariablesA { overrides?: OverridesInput; }
37+
interface VariablesB { overrides?: OverridesInput; }
38+
39+
const foo2: Unrelated & { variables: VariablesA & VariablesB } = {
40+
variables: {
41+
overrides: false // Error
42+
}
43+
};
44+
45+
46+
//// [nestedExcessPropertyChecking.js]
47+
"use strict";
48+
var ab1 = {}; // Error
49+
var ab2 = {}; // Error
50+
var E;
51+
(function (E) {
52+
E["A"] = "A";
53+
})(E || (E = {}));
54+
var x = E.A; // Error
55+
var y = "A"; // Error
56+
var foo1 = { variables: { overrides: false } }; // Error
57+
var foo2 = {
58+
variables: {
59+
overrides: false // Error
60+
}
61+
};

0 commit comments

Comments
 (0)