From 959c3aa43335ea53c411b8bbcea147b99cfc3711 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 14 Dec 2022 11:32:49 -0800 Subject: [PATCH 1/3] Fix excess property checking for intersections with index signatures --- src/compiler/checker.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7a9572c993c51..2f78939ce2e05 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20693,6 +20693,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) { inPropertyCheck = true; result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); + if (result) { + result &= indexSignaturesRelatedTo(source, target, /*sourceIsPrimitive*/ false, reportErrors, IntersectionState.None); + } inPropertyCheck = false; } } @@ -21861,7 +21864,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return result; } - function membersRelatedToIndexInfo(source: Type, targetInfo: IndexInfo, reportErrors: boolean): Ternary { + function membersRelatedToIndexInfo(source: Type, targetInfo: IndexInfo, reportErrors: boolean, intersectionState: IntersectionState): Ternary { let result = Ternary.True; const keyType = targetInfo.keyType; const props = source.flags & TypeFlags.Intersection ? getPropertiesOfUnionOrIntersectionType(source as IntersectionType) : getPropertiesOfObjectType(source); @@ -21875,7 +21878,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const type = exactOptionalPropertyTypes || propType.flags & TypeFlags.Undefined || keyType === numberType || !(prop.flags & SymbolFlags.Optional) ? propType : getTypeWithFacts(propType, TypeFacts.NEUndefined); - const related = isRelatedTo(type, targetInfo.type, RecursionFlags.Both, reportErrors); + const related = isRelatedTo(type, targetInfo.type, RecursionFlags.Both, reportErrors, /*headMessage*/ undefined, intersectionState); if (!related) { if (reportErrors) { reportError(Diagnostics.Property_0_is_incompatible_with_index_signature, symbolToString(prop)); @@ -21936,7 +21939,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } if (!(intersectionState & IntersectionState.Source) && isObjectTypeWithInferableIndex(source)) { // Intersection constituents are never considered to have an inferred index signature - return membersRelatedToIndexInfo(source, targetInfo, reportErrors); + return membersRelatedToIndexInfo(source, targetInfo, reportErrors, intersectionState); } if (reportErrors) { reportError(Diagnostics.Index_signature_for_type_0_is_missing_in_type_1, typeToString(targetInfo.keyType), typeToString(source)); From d897b56c5eff9556091c6d377f1c24cd76c03d51 Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Wed, 14 Dec 2022 11:39:34 -0800 Subject: [PATCH 2/3] Add regression tests --- ...kIntersectionWithIndexSignature.errors.txt | 29 +++++++++ ...ertyCheckIntersectionWithIndexSignature.js | 27 ++++++++ ...heckIntersectionWithIndexSignature.symbols | 50 ++++++++++++++ ...yCheckIntersectionWithIndexSignature.types | 65 +++++++++++++++++++ ...ertyCheckIntersectionWithIndexSignature.ts | 16 +++++ 5 files changed, 187 insertions(+) create mode 100644 tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.errors.txt create mode 100644 tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.js create mode 100644 tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.symbols create mode 100644 tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.types create mode 100644 tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts diff --git a/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.errors.txt b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.errors.txt new file mode 100644 index 0000000000000..b7891a0931e12 --- /dev/null +++ b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.errors.txt @@ -0,0 +1,29 @@ +tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts(5,7): error TS2322: Type '{ a: 0; }' is not assignable to type '{ a: 0; } & { b: 0; }'. + Property 'b' is missing in type '{ a: 0; }' but required in type '{ b: 0; }'. +tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts(7,24): error TS2322: Type '{ a: 0; b: 0; c: number; }' is not assignable to type '{ a: 0; } & { b: 0; }'. + Object literal may only specify known properties, and 'c' does not exist in type '{ a: 0; } & { b: 0; }'. + + +==== tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts (2 errors) ==== + // Repro from #51875 + + let x: { [x: string]: { a: 0 } } & { [x: string]: { b: 0 } }; + + x = { y: { a: 0 } }; // Error + ~ +!!! error TS2322: Type '{ a: 0; }' is not assignable to type '{ a: 0; } & { b: 0; }'. +!!! error TS2322: Property 'b' is missing in type '{ a: 0; }' but required in type '{ b: 0; }'. +!!! related TS2728 tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts:3:53: 'b' is declared here. + x = { y: { a: 0, b: 0 } }; + x = { y: { a: 0, b: 0, c: 0 } }; // Error + ~~~~ +!!! error TS2322: Type '{ a: 0; b: 0; c: number; }' is not assignable to type '{ a: 0; } & { b: 0; }'. +!!! error TS2322: Object literal may only specify known properties, and 'c' does not exist in type '{ a: 0; } & { b: 0; }'. + + type A = { a: string }; + type B = { b: string }; + + const yy: Record & Record = { + foo: { a: '', b: '' }, + }; + \ No newline at end of file diff --git a/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.js b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.js new file mode 100644 index 0000000000000..cce86d4486f8a --- /dev/null +++ b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.js @@ -0,0 +1,27 @@ +//// [excessPropertyCheckIntersectionWithIndexSignature.ts] +// Repro from #51875 + +let x: { [x: string]: { a: 0 } } & { [x: string]: { b: 0 } }; + +x = { y: { a: 0 } }; // Error +x = { y: { a: 0, b: 0 } }; +x = { y: { a: 0, b: 0, c: 0 } }; // Error + +type A = { a: string }; +type B = { b: string }; + +const yy: Record & Record = { + foo: { a: '', b: '' }, +}; + + +//// [excessPropertyCheckIntersectionWithIndexSignature.js] +"use strict"; +// Repro from #51875 +var x; +x = { y: { a: 0 } }; // Error +x = { y: { a: 0, b: 0 } }; +x = { y: { a: 0, b: 0, c: 0 } }; // Error +var yy = { + foo: { a: '', b: '' }, +}; diff --git a/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.symbols b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.symbols new file mode 100644 index 0000000000000..6f0fb54886cb5 --- /dev/null +++ b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.symbols @@ -0,0 +1,50 @@ +=== tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts === +// Repro from #51875 + +let x: { [x: string]: { a: 0 } } & { [x: string]: { b: 0 } }; +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 3)) +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 10)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 23)) +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 38)) +>b : Symbol(b, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 51)) + +x = { y: { a: 0 } }; // Error +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 3)) +>y : Symbol(y, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 4, 5)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 4, 10)) + +x = { y: { a: 0, b: 0 } }; +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 3)) +>y : Symbol(y, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 5, 5)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 5, 10)) +>b : Symbol(b, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 5, 16)) + +x = { y: { a: 0, b: 0, c: 0 } }; // Error +>x : Symbol(x, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 2, 3)) +>y : Symbol(y, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 5)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 10)) +>b : Symbol(b, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 16)) +>c : Symbol(c, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 22)) + +type A = { a: string }; +>A : Symbol(A, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 32)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 8, 10)) + +type B = { b: string }; +>B : Symbol(B, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 8, 23)) +>b : Symbol(b, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 9, 10)) + +const yy: Record & Record = { +>yy : Symbol(yy, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 11, 5)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>A : Symbol(A, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 6, 32)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>B : Symbol(B, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 8, 23)) + + foo: { a: '', b: '' }, +>foo : Symbol(foo, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 11, 51)) +>a : Symbol(a, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 12, 10)) +>b : Symbol(b, Decl(excessPropertyCheckIntersectionWithIndexSignature.ts, 12, 17)) + +}; + diff --git a/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.types b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.types new file mode 100644 index 0000000000000..d2a38ad5a5374 --- /dev/null +++ b/tests/baselines/reference/excessPropertyCheckIntersectionWithIndexSignature.types @@ -0,0 +1,65 @@ +=== tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts === +// Repro from #51875 + +let x: { [x: string]: { a: 0 } } & { [x: string]: { b: 0 } }; +>x : { [x: string]: { a: 0; }; } & { [x: string]: { b: 0; }; } +>x : string +>a : 0 +>x : string +>b : 0 + +x = { y: { a: 0 } }; // Error +>x = { y: { a: 0 } } : { y: { a: 0; }; } +>x : { [x: string]: { a: 0; }; } & { [x: string]: { b: 0; }; } +>{ y: { a: 0 } } : { y: { a: 0; }; } +>y : { a: 0; } +>{ a: 0 } : { a: 0; } +>a : 0 +>0 : 0 + +x = { y: { a: 0, b: 0 } }; +>x = { y: { a: 0, b: 0 } } : { y: { a: 0; b: 0; }; } +>x : { [x: string]: { a: 0; }; } & { [x: string]: { b: 0; }; } +>{ y: { a: 0, b: 0 } } : { y: { a: 0; b: 0; }; } +>y : { a: 0; b: 0; } +>{ a: 0, b: 0 } : { a: 0; b: 0; } +>a : 0 +>0 : 0 +>b : 0 +>0 : 0 + +x = { y: { a: 0, b: 0, c: 0 } }; // Error +>x = { y: { a: 0, b: 0, c: 0 } } : { y: { a: 0; b: 0; c: number; }; } +>x : { [x: string]: { a: 0; }; } & { [x: string]: { b: 0; }; } +>{ y: { a: 0, b: 0, c: 0 } } : { y: { a: 0; b: 0; c: number; }; } +>y : { a: 0; b: 0; c: number; } +>{ a: 0, b: 0, c: 0 } : { a: 0; b: 0; c: number; } +>a : 0 +>0 : 0 +>b : 0 +>0 : 0 +>c : number +>0 : 0 + +type A = { a: string }; +>A : { a: string; } +>a : string + +type B = { b: string }; +>B : { b: string; } +>b : string + +const yy: Record & Record = { +>yy : Record & Record +>{ foo: { a: '', b: '' },} : { foo: { a: string; b: string; }; } + + foo: { a: '', b: '' }, +>foo : { a: string; b: string; } +>{ a: '', b: '' } : { a: string; b: string; } +>a : string +>'' : "" +>b : string +>'' : "" + +}; + diff --git a/tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts b/tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts new file mode 100644 index 0000000000000..a73962fe17087 --- /dev/null +++ b/tests/cases/compiler/excessPropertyCheckIntersectionWithIndexSignature.ts @@ -0,0 +1,16 @@ +// @strict: true + +// Repro from #51875 + +let x: { [x: string]: { a: 0 } } & { [x: string]: { b: 0 } }; + +x = { y: { a: 0 } }; // Error +x = { y: { a: 0, b: 0 } }; +x = { y: { a: 0, b: 0, c: 0 } }; // Error + +type A = { a: string }; +type B = { b: string }; + +const yy: Record & Record = { + foo: { a: '', b: '' }, +}; From e6acb8fbd809598293d66d97a2fb6bc19a09241e Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Thu, 15 Dec 2022 13:22:11 -0800 Subject: [PATCH 3/3] Limit check to only fresh object literals on the source side --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2f78939ce2e05..9c51caf68dba3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20693,7 +20693,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { isNonGenericObjectType(target) && !isArrayOrTupleType(target) && source.flags & TypeFlags.Intersection && getApparentType(source).flags & TypeFlags.StructuredType && !some((source as IntersectionType).types, t => !!(getObjectFlags(t) & ObjectFlags.NonInferrableType)))) { inPropertyCheck = true; result &= propertiesRelatedTo(source, target, reportErrors, /*excludedProperties*/ undefined, IntersectionState.None); - if (result) { + if (result && isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral) { result &= indexSignaturesRelatedTo(source, target, /*sourceIsPrimitive*/ false, reportErrors, IntersectionState.None); } inPropertyCheck = false;