diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 92116ccdda172..18b73c3272569 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20876,17 +20876,41 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } let reducedTarget = target; let checkTypes: Type[] | undefined; + const excessProperties: Set = new Set(); + const assignableProperties: Set = new Set(); if (target.flags & TypeFlags.Union) { reducedTarget = findMatchingDiscriminantType(source, target as UnionType, isRelatedTo) || filterPrimitivesIfContainsNonPrimitive(target as UnionType); checkTypes = reducedTarget.flags & TypeFlags.Union ? (reducedTarget as UnionType).types : [reducedTarget]; } + // Report error in terms of object types in the target as those are the only ones + // we check in isKnownProperty. + const excessPropertyTarget = filterType(reducedTarget, isExcessPropertyCheckTarget) as UnionOrIntersectionType; + if (target.flags & TypeFlags.Union) { + for (const t of excessPropertyTarget.types) { + let typeCovered = true; + for (const prop of getPropertiesOfType(source)) { + if (!isKnownProperty(t, prop.escapedName, isComparingJsxAttributes)) { + typeCovered = false; + excessProperties.add(prop); + } + else { + assignableProperties.add(prop); + } + } + if (typeCovered) { + // Whichever type in the union covers all assigned properties will also + return false; + } + } + } for (const prop of getPropertiesOfType(source)) { + if (assignableProperties.has(prop)) { + continue; + } if (shouldCheckAsExcessProperty(prop, source.symbol) && !isIgnoredJsxProperty(source, prop)) { if (!isKnownProperty(reducedTarget, prop.escapedName, isComparingJsxAttributes)) { if (reportErrors) { - // Report error in terms of object types in the target as those are the only ones - // we check in isKnownProperty. - const errorTarget = filterType(reducedTarget, isExcessPropertyCheckTarget); + // We know *exactly* where things went wrong when comparing the types. // Use this property as the error node as this will be more helpful in // reasoning about what went wrong. @@ -20900,13 +20924,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { errorNode = prop.valueDeclaration.name; } const propName = symbolToString(prop); - const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, errorTarget); + const suggestionSymbol = getSuggestedSymbolForNonexistentJSXAttribute(propName, excessPropertyTarget); const suggestion = suggestionSymbol ? symbolToString(suggestionSymbol) : undefined; if (suggestion) { - reportError(Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(errorTarget), suggestion); + reportError(Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2, propName, typeToString(excessPropertyTarget), suggestion); } else { - reportError(Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(errorTarget)); + reportError(Diagnostics.Property_0_does_not_exist_on_type_1, propName, typeToString(excessPropertyTarget)); } } else { @@ -20921,16 +20945,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { errorNode = name; if (isIdentifier(name)) { - suggestion = getSuggestionForNonexistentProperty(name, errorTarget); + suggestion = getSuggestionForNonexistentProperty(name, excessPropertyTarget); } } if (suggestion !== undefined) { reportError(Diagnostics.Object_literal_may_only_specify_known_properties_but_0_does_not_exist_in_type_1_Did_you_mean_to_write_2, - symbolToString(prop), typeToString(errorTarget), suggestion); + symbolToString(prop), typeToString(excessPropertyTarget), suggestion); } else { reportError(Diagnostics.Object_literal_may_only_specify_known_properties_and_0_does_not_exist_in_type_1, - symbolToString(prop), typeToString(errorTarget)); + symbolToString(prop), typeToString(excessPropertyTarget)); } } } @@ -20944,6 +20968,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } } + const undecidedProperties = [...excessProperties].filter((p: any) => assignableProperties.has(p)); + if (undecidedProperties.length) { + if (reportErrors) { + reportError(Diagnostics.Properties_1_can_t_be_assigned_to_any_type_in_union_2_and_make_0_unassignable, typeToString(source), undecidedProperties.join(", "), typeToString(excessPropertyTarget)); + } + return true; + } return false; } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0d01c46edd7b5..849b36d287622 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3659,6 +3659,10 @@ "category": "Error", "code": 2854 }, + "Properties '{1}' can't be assigned to any type in union '{2}' and make '{0}' unassignable": { + "category": "Error", + "code": 2855 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", diff --git a/tests/baselines/reference/unionTypeExcessPropertyCheck.errors.txt b/tests/baselines/reference/unionTypeExcessPropertyCheck.errors.txt new file mode 100644 index 0000000000000..287df2aa88bdb --- /dev/null +++ b/tests/baselines/reference/unionTypeExcessPropertyCheck.errors.txt @@ -0,0 +1,62 @@ +unionTypeExcessPropertyCheck.ts(11,9): error TS2322: Type '{ b: string; c: string; }' is not assignable to type 'AC'. + Object literal may only specify known properties, and 'b' does not exist in type 'AC'. +unionTypeExcessPropertyCheck.ts(17,9): error TS2322: Type '{ b: string; c: string; }' is not assignable to type 'B'. + Object literal may only specify known properties, and 'c' does not exist in type 'B'. +unionTypeExcessPropertyCheck.ts(27,5): error TS2322: Type '{ b: string; x: string; }' is not assignable to type 'AC | B'. + Object literal may only specify known properties, and 'x' does not exist in type 'AC | B'. + Excess properties detected in Object literal '{ b: string; x: string; }' combination of properties '[object Object]' make type 'AC | B' undeducible +unionTypeExcessPropertyCheck.ts(33,5): error TS2322: Type '{ a: string; c: string; x: string; }' is not assignable to type 'AC | B'. + Object literal may only specify known properties, and 'x' does not exist in type 'AC | B'. + Excess properties detected in Object literal '{ a: string; c: string; x: string; }' combination of properties '[object Object], [object Object]' make type 'AC | B' undeducible + + +==== unionTypeExcessPropertyCheck.ts (4 errors) ==== + type AC = { + a: string, + c: string + }; + type B = { + b: string + }; + + // Fails correctly as `b` is not in `AC` + const ac_b: AC = { + b: '', + ~ +!!! error TS2322: Type '{ b: string; c: string; }' is not assignable to type 'AC'. +!!! error TS2322: Object literal may only specify known properties, and 'b' does not exist in type 'AC'. + c: '' + }; + // Fails correctly as `c` is not in `B` + const b_c: B = { + b: '', + c: '' + ~ +!!! error TS2322: Type '{ b: string; c: string; }' is not assignable to type 'B'. +!!! error TS2322: Object literal may only specify known properties, and 'c' does not exist in type 'B'. + }; + // Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead + const acb_bc: AC|B = { + b: '', + c: '' + }; + // Fails correctly as `x` in in neither `AC` nor `B` + const acb_bx: AC|B = { + b: '', + x: '' + ~ +!!! error TS2322: Type '{ b: string; x: string; }' is not assignable to type 'AC | B'. +!!! error TS2322: Object literal may only specify known properties, and 'x' does not exist in type 'AC | B'. +!!! error TS2322: Excess properties detected in Object literal '{ b: string; x: string; }' combination of properties '[object Object]' make type 'AC | B' undeducible + }; + // Fails correctly as `x` in in neither `AC` nor `B` + const acb_acx: AC|B = { + a: '', + c: '', + x: '' + ~ +!!! error TS2322: Type '{ a: string; c: string; x: string; }' is not assignable to type 'AC | B'. +!!! error TS2322: Object literal may only specify known properties, and 'x' does not exist in type 'AC | B'. +!!! error TS2322: Excess properties detected in Object literal '{ a: string; c: string; x: string; }' combination of properties '[object Object], [object Object]' make type 'AC | B' undeducible + }; + \ No newline at end of file diff --git a/tests/baselines/reference/unionTypeExcessPropertyCheck.js b/tests/baselines/reference/unionTypeExcessPropertyCheck.js new file mode 100644 index 0000000000000..b58e2cbe59540 --- /dev/null +++ b/tests/baselines/reference/unionTypeExcessPropertyCheck.js @@ -0,0 +1,66 @@ +//// [tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts] //// + +//// [unionTypeExcessPropertyCheck.ts] +type AC = { + a: string, + c: string +}; +type B = { + b: string +}; + +// Fails correctly as `b` is not in `AC` +const ac_b: AC = { + b: '', + c: '' +}; +// Fails correctly as `c` is not in `B` +const b_c: B = { + b: '', + c: '' +}; +// Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead +const acb_bc: AC|B = { + b: '', + c: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_bx: AC|B = { + b: '', + x: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_acx: AC|B = { + a: '', + c: '', + x: '' +}; + + +//// [unionTypeExcessPropertyCheck.js] +// Fails correctly as `b` is not in `AC` +var ac_b = { + b: '', + c: '' +}; +// Fails correctly as `c` is not in `B` +var b_c = { + b: '', + c: '' +}; +// Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead +var acb_bc = { + b: '', + c: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +var acb_bx = { + b: '', + x: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +var acb_acx = { + a: '', + c: '', + x: '' +}; diff --git a/tests/baselines/reference/unionTypeExcessPropertyCheck.symbols b/tests/baselines/reference/unionTypeExcessPropertyCheck.symbols new file mode 100644 index 0000000000000..bb1f20d7c691f --- /dev/null +++ b/tests/baselines/reference/unionTypeExcessPropertyCheck.symbols @@ -0,0 +1,88 @@ +//// [tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts] //// + +=== unionTypeExcessPropertyCheck.ts === +type AC = { +>AC : Symbol(AC, Decl(unionTypeExcessPropertyCheck.ts, 0, 0)) + + a: string, +>a : Symbol(a, Decl(unionTypeExcessPropertyCheck.ts, 0, 11)) + + c: string +>c : Symbol(c, Decl(unionTypeExcessPropertyCheck.ts, 1, 14)) + +}; +type B = { +>B : Symbol(B, Decl(unionTypeExcessPropertyCheck.ts, 3, 2)) + + b: string +>b : Symbol(b, Decl(unionTypeExcessPropertyCheck.ts, 4, 10)) + +}; + +// Fails correctly as `b` is not in `AC` +const ac_b: AC = { +>ac_b : Symbol(ac_b, Decl(unionTypeExcessPropertyCheck.ts, 9, 5)) +>AC : Symbol(AC, Decl(unionTypeExcessPropertyCheck.ts, 0, 0)) + + b: '', +>b : Symbol(b, Decl(unionTypeExcessPropertyCheck.ts, 9, 18)) + + c: '' +>c : Symbol(c, Decl(unionTypeExcessPropertyCheck.ts, 10, 14)) + +}; +// Fails correctly as `c` is not in `B` +const b_c: B = { +>b_c : Symbol(b_c, Decl(unionTypeExcessPropertyCheck.ts, 14, 5)) +>B : Symbol(B, Decl(unionTypeExcessPropertyCheck.ts, 3, 2)) + + b: '', +>b : Symbol(b, Decl(unionTypeExcessPropertyCheck.ts, 14, 16)) + + c: '' +>c : Symbol(c, Decl(unionTypeExcessPropertyCheck.ts, 15, 14)) + +}; +// Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead +const acb_bc: AC|B = { +>acb_bc : Symbol(acb_bc, Decl(unionTypeExcessPropertyCheck.ts, 19, 5)) +>AC : Symbol(AC, Decl(unionTypeExcessPropertyCheck.ts, 0, 0)) +>B : Symbol(B, Decl(unionTypeExcessPropertyCheck.ts, 3, 2)) + + b: '', +>b : Symbol(b, Decl(unionTypeExcessPropertyCheck.ts, 19, 22)) + + c: '' +>c : Symbol(c, Decl(unionTypeExcessPropertyCheck.ts, 20, 14)) + +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_bx: AC|B = { +>acb_bx : Symbol(acb_bx, Decl(unionTypeExcessPropertyCheck.ts, 24, 5)) +>AC : Symbol(AC, Decl(unionTypeExcessPropertyCheck.ts, 0, 0)) +>B : Symbol(B, Decl(unionTypeExcessPropertyCheck.ts, 3, 2)) + + b: '', +>b : Symbol(b, Decl(unionTypeExcessPropertyCheck.ts, 24, 22)) + + x: '' +>x : Symbol(x, Decl(unionTypeExcessPropertyCheck.ts, 25, 10)) + +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_acx: AC|B = { +>acb_acx : Symbol(acb_acx, Decl(unionTypeExcessPropertyCheck.ts, 29, 5)) +>AC : Symbol(AC, Decl(unionTypeExcessPropertyCheck.ts, 0, 0)) +>B : Symbol(B, Decl(unionTypeExcessPropertyCheck.ts, 3, 2)) + + a: '', +>a : Symbol(a, Decl(unionTypeExcessPropertyCheck.ts, 29, 23)) + + c: '', +>c : Symbol(c, Decl(unionTypeExcessPropertyCheck.ts, 30, 10)) + + x: '' +>x : Symbol(x, Decl(unionTypeExcessPropertyCheck.ts, 31, 10)) + +}; + diff --git a/tests/baselines/reference/unionTypeExcessPropertyCheck.types b/tests/baselines/reference/unionTypeExcessPropertyCheck.types new file mode 100644 index 0000000000000..2188212bc40dc --- /dev/null +++ b/tests/baselines/reference/unionTypeExcessPropertyCheck.types @@ -0,0 +1,96 @@ +//// [tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts] //// + +=== unionTypeExcessPropertyCheck.ts === +type AC = { +>AC : { a: string; c: string; } + + a: string, +>a : string + + c: string +>c : string + +}; +type B = { +>B : { b: string; } + + b: string +>b : string + +}; + +// Fails correctly as `b` is not in `AC` +const ac_b: AC = { +>ac_b : AC +>{ b: '', c: ''} : { b: string; c: string; } + + b: '', +>b : string +>'' : "" + + c: '' +>c : string +>'' : "" + +}; +// Fails correctly as `c` is not in `B` +const b_c: B = { +>b_c : B +>{ b: '', c: ''} : { b: string; c: string; } + + b: '', +>b : string +>'' : "" + + c: '' +>c : string +>'' : "" + +}; +// Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead +const acb_bc: AC|B = { +>acb_bc : AC | B +>{ b: '', c: ''} : { b: string; c: string; } + + b: '', +>b : string +>'' : "" + + c: '' +>c : string +>'' : "" + +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_bx: AC|B = { +>acb_bx : AC | B +>{ b: '', x: ''} : { b: string; x: string; } + + b: '', +>b : string +>'' : "" + + x: '' +>x : string +>'' : "" + +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_acx: AC|B = { +>acb_acx : AC | B +>{ a: '', c: '', x: ''} : { a: string; c: string; x: string; } + + a: '', +>a : string +>'' : "" + + c: '', +>c : string +>'' : "" + + x: '' +>x : string +>'' : "" + +}; + diff --git a/tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts b/tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts new file mode 100644 index 0000000000000..cc83de34da7ce --- /dev/null +++ b/tests/cases/conformance/types/union/unionTypeExcessPropertyCheck.ts @@ -0,0 +1,34 @@ +type AC = { + a: string, + c: string +}; +type B = { + b: string +}; + +// Fails correctly as `b` is not in `AC` +const ac_b: AC = { + b: '', + c: '' +}; +// Fails correctly as `c` is not in `B` +const b_c: B = { + b: '', + c: '' +}; +// Should fail because `c` is not in `B` while `b` is not in `AB`, but works instead +const acb_bc: AC|B = { + b: '', + c: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_bx: AC|B = { + b: '', + x: '' +}; +// Fails correctly as `x` in in neither `AC` nor `B` +const acb_acx: AC|B = { + a: '', + c: '', + x: '' +};