diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f3658c645db..26f0baec5ec 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -9225,7 +9225,8 @@ namespace ts { isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True; if (isObjectLiteralType(source) && source.flags & TypeFlags.FreshLiteral) { - if (hasExcessProperties(source, target, reportErrors)) { + const discriminantType = target.flags & TypeFlags.Union ? findMatchingDiscriminantType(source, target as UnionType) : undefined; + if (hasExcessProperties(source, target, discriminantType, reportErrors)) { if (reportErrors) { reportRelationError(headMessage, source, target); } @@ -9235,7 +9236,7 @@ namespace ts { // and intersection types are further deconstructed on the target side, we don't want to // make the check again (as it might fail for a partial target type). Therefore we obtain // the regular source type and proceed with that. - if (isUnionOrIntersectionTypeWithoutNullableConstituents(target)) { + if (isUnionOrIntersectionTypeWithoutNullableConstituents(target) && !discriminantType) { source = getRegularTypeOfObjectLiteral(source); } } @@ -9336,19 +9337,16 @@ namespace ts { return Ternary.False; } - function hasExcessProperties(source: FreshObjectLiteralType, target: Type, reportErrors: boolean): boolean { + function hasExcessProperties(source: FreshObjectLiteralType, target: Type, discriminant: Type | undefined, reportErrors: boolean): boolean { if (maybeTypeOfKind(target, TypeFlags.Object) && !(getObjectFlags(target) & ObjectFlags.ObjectLiteralPatternWithComputedProperties)) { const isComparingJsxAttributes = !!(source.flags & TypeFlags.JsxAttributes); if ((relation === assignableRelation || relation === comparableRelation) && (isTypeSubsetOf(globalObjectType, target) || (!isComparingJsxAttributes && isEmptyObjectType(target)))) { return false; } - if (target.flags & TypeFlags.Union) { - const discriminantType = findMatchingDiscriminantType(source, target as UnionType); - if (discriminantType) { - // check excess properties against discriminant type only, not the entire union - return hasExcessProperties(source, discriminantType, reportErrors); - } + if (discriminant) { + // check excess properties against discriminant type only, not the entire union + return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors); } for (const prop of getPropertiesOfObjectType(source)) { if (!isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) { diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt b/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt index 24940d173d0..f5e0550033d 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt @@ -21,9 +21,14 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(49,35): error TS2322: Type Object literal may only specify known properties, and 'second' does not exist in type '{ a: 1; b: 1; first: string; }'. tests/cases/compiler/excessPropertyCheckWithUnions.ts(50,35): error TS2322: Type '{ a: 1; b: 1; first: string; third: string; }' is not assignable to type 'Overlapping'. Object literal may only specify known properties, and 'third' does not exist in type '{ a: 1; b: 1; first: string; }'. +tests/cases/compiler/excessPropertyCheckWithUnions.ts(66,9): error TS2322: Type '{ kind: "A"; n: { a: string; b: string; }; }' is not assignable to type 'AB'. + Type '{ kind: "A"; n: { a: string; b: string; }; }' is not assignable to type '{ kind: "A"; n: AN; }'. + Types of property 'n' are incompatible. + Type '{ a: string; b: string; }' is not assignable to type 'AN'. + Object literal may only specify known properties, and 'b' does not exist in type 'AN'. -==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (9 errors) ==== +==== tests/cases/compiler/excessPropertyCheckWithUnions.ts (10 errors) ==== type ADT = { tag: "A", a1: string @@ -112,4 +117,29 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(50,35): error TS2322: Type declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } let t2 = { ...t1 } t0 = t2 + + // Nested excess property checks work with discriminated unions + type AN = { a: string } | { c: string } + type BN = { b: string } + type AB = { kind: "A", n: AN } | { kind: "B", n: BN } + const abab: AB = { + kind: "A", + n: { + a: "a", + b: "b", // excess -- kind: "A" + ~~~~~~ +!!! error TS2322: Type '{ kind: "A"; n: { a: string; b: string; }; }' is not assignable to type 'AB'. +!!! error TS2322: Type '{ kind: "A"; n: { a: string; b: string; }; }' is not assignable to type '{ kind: "A"; n: AN; }'. +!!! error TS2322: Types of property 'n' are incompatible. +!!! error TS2322: Type '{ a: string; b: string; }' is not assignable to type 'AN'. +!!! error TS2322: Object literal may only specify known properties, and 'b' does not exist in type 'AN'. + } + } + const abac: AB = { + kind: "A", + n: { + a: "a", + c: "c", // ok -- kind: "A", an: { a: string } | { c: string } + } + } \ No newline at end of file diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.js b/tests/baselines/reference/excessPropertyCheckWithUnions.js index a20983e4b08..1ac4d7477d6 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.js +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.js @@ -55,6 +55,25 @@ declare let t0: { a: any, b: any } | { d: any, e: any } declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } let t2 = { ...t1 } t0 = t2 + +// Nested excess property checks work with discriminated unions +type AN = { a: string } | { c: string } +type BN = { b: string } +type AB = { kind: "A", n: AN } | { kind: "B", n: BN } +const abab: AB = { + kind: "A", + n: { + a: "a", + b: "b", // excess -- kind: "A" + } +} +const abac: AB = { + kind: "A", + n: { + a: "a", + c: "c", // ok -- kind: "A", an: { a: string } | { c: string } + } +} //// [excessPropertyCheckWithUnions.js] @@ -89,3 +108,17 @@ over = { a: 1, b: 1, first: "ok", second: "error" }; over = { a: 1, b: 1, first: "ok", third: "error" }; var t2 = __assign({}, t1); t0 = t2; +var abab = { + kind: "A", + n: { + a: "a", + b: "b" + } +}; +var abac = { + kind: "A", + n: { + a: "a", + c: "c" + } +}; diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.symbols b/tests/baselines/reference/excessPropertyCheckWithUnions.symbols index 381681de384..beee933a2f1 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.symbols +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.symbols @@ -167,3 +167,57 @@ t0 = t2 >t0 : Symbol(t0, Decl(excessPropertyCheckWithUnions.ts, 52, 11)) >t2 : Symbol(t2, Decl(excessPropertyCheckWithUnions.ts, 54, 3)) +// Nested excess property checks work with discriminated unions +type AN = { a: string } | { c: string } +>AN : Symbol(AN, Decl(excessPropertyCheckWithUnions.ts, 55, 7)) +>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 58, 11)) +>c : Symbol(c, Decl(excessPropertyCheckWithUnions.ts, 58, 27)) + +type BN = { b: string } +>BN : Symbol(BN, Decl(excessPropertyCheckWithUnions.ts, 58, 39)) +>b : Symbol(b, Decl(excessPropertyCheckWithUnions.ts, 59, 11)) + +type AB = { kind: "A", n: AN } | { kind: "B", n: BN } +>AB : Symbol(AB, Decl(excessPropertyCheckWithUnions.ts, 59, 23)) +>kind : Symbol(kind, Decl(excessPropertyCheckWithUnions.ts, 60, 11)) +>n : Symbol(n, Decl(excessPropertyCheckWithUnions.ts, 60, 22)) +>AN : Symbol(AN, Decl(excessPropertyCheckWithUnions.ts, 55, 7)) +>kind : Symbol(kind, Decl(excessPropertyCheckWithUnions.ts, 60, 34)) +>n : Symbol(n, Decl(excessPropertyCheckWithUnions.ts, 60, 45)) +>BN : Symbol(BN, Decl(excessPropertyCheckWithUnions.ts, 58, 39)) + +const abab: AB = { +>abab : Symbol(abab, Decl(excessPropertyCheckWithUnions.ts, 61, 5)) +>AB : Symbol(AB, Decl(excessPropertyCheckWithUnions.ts, 59, 23)) + + kind: "A", +>kind : Symbol(kind, Decl(excessPropertyCheckWithUnions.ts, 61, 18)) + + n: { +>n : Symbol(n, Decl(excessPropertyCheckWithUnions.ts, 62, 14)) + + a: "a", +>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 63, 8)) + + b: "b", // excess -- kind: "A" +>b : Symbol(b, Decl(excessPropertyCheckWithUnions.ts, 64, 15)) + } +} +const abac: AB = { +>abac : Symbol(abac, Decl(excessPropertyCheckWithUnions.ts, 68, 5)) +>AB : Symbol(AB, Decl(excessPropertyCheckWithUnions.ts, 59, 23)) + + kind: "A", +>kind : Symbol(kind, Decl(excessPropertyCheckWithUnions.ts, 68, 18)) + + n: { +>n : Symbol(n, Decl(excessPropertyCheckWithUnions.ts, 69, 14)) + + a: "a", +>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 70, 8)) + + c: "c", // ok -- kind: "A", an: { a: string } | { c: string } +>c : Symbol(c, Decl(excessPropertyCheckWithUnions.ts, 71, 15)) + } +} + diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.types b/tests/baselines/reference/excessPropertyCheckWithUnions.types index 4ef53e35ecc..41a8bb67a03 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.types +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.types @@ -221,3 +221,67 @@ t0 = t2 >t0 : { a: any; b: any; } | { d: any; e: any; } >t2 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } +// Nested excess property checks work with discriminated unions +type AN = { a: string } | { c: string } +>AN : AN +>a : string +>c : string + +type BN = { b: string } +>BN : BN +>b : string + +type AB = { kind: "A", n: AN } | { kind: "B", n: BN } +>AB : AB +>kind : "A" +>n : AN +>AN : AN +>kind : "B" +>n : BN +>BN : BN + +const abab: AB = { +>abab : AB +>AB : AB +>{ kind: "A", n: { a: "a", b: "b", // excess -- kind: "A" }} : { kind: "A"; n: { a: string; b: string; }; } + + kind: "A", +>kind : "A" +>"A" : "A" + + n: { +>n : { a: string; b: string; } +>{ a: "a", b: "b", // excess -- kind: "A" } : { a: string; b: string; } + + a: "a", +>a : string +>"a" : "a" + + b: "b", // excess -- kind: "A" +>b : string +>"b" : "b" + } +} +const abac: AB = { +>abac : AB +>AB : AB +>{ kind: "A", n: { a: "a", c: "c", // ok -- kind: "A", an: { a: string } | { c: string } }} : { kind: "A"; n: { a: string; c: string; }; } + + kind: "A", +>kind : "A" +>"A" : "A" + + n: { +>n : { a: string; c: string; } +>{ a: "a", c: "c", // ok -- kind: "A", an: { a: string } | { c: string } } : { a: string; c: string; } + + a: "a", +>a : string +>"a" : "a" + + c: "c", // ok -- kind: "A", an: { a: string } | { c: string } +>c : string +>"c" : "c" + } +} + diff --git a/tests/cases/compiler/excessPropertyCheckWithUnions.ts b/tests/cases/compiler/excessPropertyCheckWithUnions.ts index 240af391cf5..027e67e4ec9 100644 --- a/tests/cases/compiler/excessPropertyCheckWithUnions.ts +++ b/tests/cases/compiler/excessPropertyCheckWithUnions.ts @@ -55,3 +55,22 @@ declare let t0: { a: any, b: any } | { d: any, e: any } declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } let t2 = { ...t1 } t0 = t2 + +// Nested excess property checks work with discriminated unions +type AN = { a: string } | { c: string } +type BN = { b: string } +type AB = { kind: "A", n: AN } | { kind: "B", n: BN } +const abab: AB = { + kind: "A", + n: { + a: "a", + b: "b", // excess -- kind: "A" + } +} +const abac: AB = { + kind: "A", + n: { + a: "a", + c: "c", // ok -- kind: "A", an: { a: string } | { c: string } + } +}