From d71cc12b6179cde0db515207b3da57d41b4b04a2 Mon Sep 17 00:00:00 2001 From: GokhanKabar Date: Sat, 14 Mar 2026 14:31:58 +0100 Subject: [PATCH] Fix exactOptionalPropertyTypes not enforced through spread-with-ternary (#63240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When spreading a conditional expression like `{ ...(cond ? { x: obj.optProp } : {}) }` where `optProp` is an optional property, the exactOptionalPropertyTypes check was silently bypassed. The value of an optional property read (e.g. `obj.optProp`) has the internal type `T | missingType`. In `getAnonymousPartialType`, calling `addOptionality` on that type is a no-op because `missingType` is already the first union member, producing a spread type identical to the target's exact optional property type and suppressing the diagnostic. Fix: when a *required* property's type contains `missingType`, normalize it to `undefinedType` before calling `addOptionality`, so the resulting type is `T | undefined | missingType` — distinguishable from the target — and the check fires. --- src/compiler/checker.ts | 6 +- ...onalPropertyTypes_spreadTernary.errors.txt | 80 +++++++ ...xactOptionalPropertyTypes_spreadTernary.js | 69 ++++++ ...ptionalPropertyTypes_spreadTernary.symbols | 99 ++++++++ ...tOptionalPropertyTypes_spreadTernary.types | 226 ++++++++++++++++++ ...xactOptionalPropertyTypes_spreadTernary.ts | 42 ++++ 6 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.errors.txt create mode 100644 tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.js create mode 100644 tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.symbols create mode 100644 tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.types create mode 100644 tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..a1c4f842b2543 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20135,7 +20135,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor); const flags = SymbolFlags.Property | SymbolFlags.Optional; const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? CheckFlags.Readonly : 0)); - result.links.type = isSetonlyAccessor ? undefinedType : addOptionality(getTypeOfSymbol(prop), /*isProperty*/ true); + let propType = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop); + if (exactOptionalPropertyTypes && !isSetonlyAccessor && !(prop.flags & SymbolFlags.Optional) && containsMissingType(propType)) { + propType = getUnionType([removeMissingOrUndefinedType(propType), undefinedType]); + } + result.links.type = isSetonlyAccessor ? undefinedType : addOptionality(propType, /*isProperty*/ true); result.declarations = prop.declarations; result.links.nameType = getSymbolLinks(prop).nameType; result.links.syntheticOrigin = prop; diff --git a/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.errors.txt b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.errors.txt new file mode 100644 index 0000000000000..388ab65560125 --- /dev/null +++ b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.errors.txt @@ -0,0 +1,80 @@ +exactOptionalPropertyTypes_spreadTernary.ts(13,1): error TS2375: Type '{ parentId: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'parentId' are incompatible. + Type 'string | undefined' is not assignable to type 'string'. + Type 'undefined' is not assignable to type 'string'. +exactOptionalPropertyTypes_spreadTernary.ts(16,1): error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'parentId' are incompatible. + Type 'string | undefined' is not assignable to type 'string'. + Type 'undefined' is not assignable to type 'string'. +exactOptionalPropertyTypes_spreadTernary.ts(20,1): error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'parentId' are incompatible. + Type 'string | undefined' is not assignable to type 'string'. + Type 'undefined' is not assignable to type 'string'. +exactOptionalPropertyTypes_spreadTernary.ts(24,1): error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'parentId' are incompatible. + Type 'string | undefined' is not assignable to type 'string'. + Type 'undefined' is not assignable to type 'string'. + + +==== exactOptionalPropertyTypes_spreadTernary.ts (4 errors) ==== + // Repro from https://github.com/microsoft/TypeScript/issues/63240 + // exactOptionalPropertyTypes should flag optional-property values spread via ternary + + type Foo = { + parentId?: string; + }; + + declare const requestBody: Foo; + declare const cond: boolean; + let target: Foo; + + // Direct assignment — correctly flagged + target = { parentId: requestBody.parentId }; // Error + ~~~~~~ +!!! error TS2375: Type '{ parentId: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'parentId' are incompatible. +!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'. +!!! error TS2375: Type 'undefined' is not assignable to type 'string'. + + // Spread + ternary with optional property access — must also be flagged + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error + ~~~~~~ +!!! error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'parentId' are incompatible. +!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'. +!!! error TS2375: Type 'undefined' is not assignable to type 'string'. + + // Destructured optional property — must also be flagged + const { parentId } = requestBody; + target = { ...(cond ? { parentId } : {}) }; // Error + ~~~~~~ +!!! error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'parentId' are incompatible. +!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'. +!!! error TS2375: Type 'undefined' is not assignable to type 'string'. + + // Explicit `string | undefined` value — must be flagged (was already working) + const parentId2 = '' as string | undefined; + target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error + ~~~~~~ +!!! error TS2375: Type '{ parentId?: string | undefined; }' is not assignable to type 'Foo' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'parentId' are incompatible. +!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'. +!!! error TS2375: Type 'undefined' is not assignable to type 'string'. + + // ------------------------------------------------------------------ + // Valid cases — must NOT produce errors + // ------------------------------------------------------------------ + + // Spreading the whole Foo object is fine (its optional properties are already correctly typed) + target = { ...(cond ? requestBody : {}) }; // OK + + // A required string value is fine + const parentId3 = 'hello'; + target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK + + // Spreading an object where the property is narrowed to string + if (cond && requestBody.parentId !== undefined) { + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string + } + \ No newline at end of file diff --git a/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.js b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.js new file mode 100644 index 0000000000000..6fc539bb2823d --- /dev/null +++ b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.js @@ -0,0 +1,69 @@ +//// [tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts] //// + +//// [exactOptionalPropertyTypes_spreadTernary.ts] +type Foo = { + parentId?: string; +}; + +declare const requestBody: Foo; +declare const cond: boolean; +let target: Foo; + +// Direct assignment — correctly flagged +target = { parentId: requestBody.parentId }; // Error + +// Spread + ternary with optional property access — must also be flagged +target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error + +// Destructured optional property — must also be flagged +const { parentId } = requestBody; +target = { ...(cond ? { parentId } : {}) }; // Error + +// Explicit `string | undefined` value — must be flagged (was already working) +const parentId2 = '' as string | undefined; +target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error + +// ------------------------------------------------------------------ +// Valid cases — must NOT produce errors +// ------------------------------------------------------------------ + +// Spreading the whole Foo object is fine (its optional properties are already correctly typed) +target = { ...(cond ? requestBody : {}) }; // OK + +// A required string value is fine +const parentId3 = 'hello'; +target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK + +// Spreading an object where the property is narrowed to string +if (cond && requestBody.parentId !== undefined) { + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string +} + + +//// [exactOptionalPropertyTypes_spreadTernary.js] +"use strict"; +// Repro from https://github.com/microsoft/TypeScript/issues/63240 +// exactOptionalPropertyTypes should flag optional-property values spread via ternary +let target; +// Direct assignment — correctly flagged +target = { parentId: requestBody.parentId }; // Error +// Spread + ternary with optional property access — must also be flagged +target = Object.assign({}, (cond ? { parentId: requestBody.parentId } : {})); // Error +// Destructured optional property — must also be flagged +const { parentId } = requestBody; +target = Object.assign({}, (cond ? { parentId } : {})); // Error +// Explicit `string | undefined` value — must be flagged (was already working) +const parentId2 = ''; +target = Object.assign({}, (cond ? { parentId: parentId2 } : {})); // Error +// ------------------------------------------------------------------ +// Valid cases — must NOT produce errors +// ------------------------------------------------------------------ +// Spreading the whole Foo object is fine (its optional properties are already correctly typed) +target = Object.assign({}, (cond ? requestBody : {})); // OK +// A required string value is fine +const parentId3 = 'hello'; +target = Object.assign({}, (cond ? { parentId: parentId3 } : {})); // OK +// Spreading an object where the property is narrowed to string +if (cond && requestBody.parentId !== undefined) { + target = Object.assign({}, (cond ? { parentId: requestBody.parentId } : {})); // OK — narrowed to string +} diff --git a/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.symbols b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.symbols new file mode 100644 index 0000000000000..dfae2945f358b --- /dev/null +++ b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.symbols @@ -0,0 +1,99 @@ +//// [tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts] //// + +=== exactOptionalPropertyTypes_spreadTernary.ts === +// Repro from https://github.com/microsoft/TypeScript/issues/63240 +// exactOptionalPropertyTypes should flag optional-property values spread via ternary + +type Foo = { +>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0)) + + parentId?: string; +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) + +}; + +declare const requestBody: Foo; +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) +>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0)) + +declare const cond: boolean; +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) + +let target: Foo; +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0)) + +// Direct assignment — correctly flagged +target = { parentId: requestBody.parentId }; // Error +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 12, 10)) +>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) + +// Spread + ternary with optional property access — must also be flagged +target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 15, 23)) +>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) + +// Destructured optional property — must also be flagged +const { parentId } = requestBody; +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 18, 7)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) + +target = { ...(cond ? { parentId } : {}) }; // Error +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 19, 23)) + +// Explicit `string | undefined` value — must be flagged (was already working) +const parentId2 = '' as string | undefined; +>parentId2 : Symbol(parentId2, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 22, 5)) + +target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 23, 23)) +>parentId2 : Symbol(parentId2, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 22, 5)) + +// ------------------------------------------------------------------ +// Valid cases — must NOT produce errors +// ------------------------------------------------------------------ + +// Spreading the whole Foo object is fine (its optional properties are already correctly typed) +target = { ...(cond ? requestBody : {}) }; // OK +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) + +// A required string value is fine +const parentId3 = 'hello'; +>parentId3 : Symbol(parentId3, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 33, 5)) + +target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 34, 23)) +>parentId3 : Symbol(parentId3, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 33, 5)) + +// Spreading an object where the property is narrowed to string +if (cond && requestBody.parentId !== undefined) { +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +>undefined : Symbol(undefined) + + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string +>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3)) +>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 38, 27)) +>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13)) +>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12)) +} + diff --git a/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.types b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.types new file mode 100644 index 0000000000000..5af419fdde55e --- /dev/null +++ b/tests/baselines/reference/exactOptionalPropertyTypes_spreadTernary.types @@ -0,0 +1,226 @@ +//// [tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts] //// + +=== exactOptionalPropertyTypes_spreadTernary.ts === +// Repro from https://github.com/microsoft/TypeScript/issues/63240 +// exactOptionalPropertyTypes should flag optional-property values spread via ternary + +type Foo = { +>Foo : Foo +> : ^^^ + + parentId?: string; +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ + +}; + +declare const requestBody: Foo; +>requestBody : Foo +> : ^^^ + +declare const cond: boolean; +>cond : boolean +> : ^^^^^^^ + +let target: Foo; +>target : Foo +> : ^^^ + +// Direct assignment — correctly flagged +target = { parentId: requestBody.parentId }; // Error +>target = { parentId: requestBody.parentId } : { parentId: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ parentId: requestBody.parentId } : { parentId: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody.parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody : Foo +> : ^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ + +// Spread + ternary with optional property access — must also be flagged +target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error +>target = { ...(cond ? { parentId: requestBody.parentId } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? { parentId: requestBody.parentId } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>(cond ? { parentId: requestBody.parentId } : {}) : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond ? { parentId: requestBody.parentId } : {} : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond : boolean +> : ^^^^^^^ +>{ parentId: requestBody.parentId } : { parentId: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody.parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody : Foo +> : ^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>{} : {} +> : ^^ + +// Destructured optional property — must also be flagged +const { parentId } = requestBody; +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody : Foo +> : ^^^ + +target = { ...(cond ? { parentId } : {}) }; // Error +>target = { ...(cond ? { parentId } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? { parentId } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>(cond ? { parentId } : {}) : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond ? { parentId } : {} : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond : boolean +> : ^^^^^^^ +>{ parentId } : { parentId: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>{} : {} +> : ^^ + +// Explicit `string | undefined` value — must be flagged (was already working) +const parentId2 = '' as string | undefined; +>parentId2 : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>'' as string | undefined : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>'' : "" +> : ^^ + +target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error +>target = { ...(cond ? { parentId: parentId2 } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? { parentId: parentId2 } : {}) } : { parentId?: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>(cond ? { parentId: parentId2 } : {}) : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond ? { parentId: parentId2 } : {} : { parentId: string | undefined; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond : boolean +> : ^^^^^^^ +>{ parentId: parentId2 } : { parentId: string | undefined; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>parentId2 : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>{} : {} +> : ^^ + +// ------------------------------------------------------------------ +// Valid cases — must NOT produce errors +// ------------------------------------------------------------------ + +// Spreading the whole Foo object is fine (its optional properties are already correctly typed) +target = { ...(cond ? requestBody : {}) }; // OK +>target = { ...(cond ? requestBody : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^ ^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? requestBody : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^ ^^^ +>(cond ? requestBody : {}) : Foo +> : ^^^ +>cond ? requestBody : {} : Foo +> : ^^^ +>cond : boolean +> : ^^^^^^^ +>requestBody : Foo +> : ^^^ +>{} : {} +> : ^^ + +// A required string value is fine +const parentId3 = 'hello'; +>parentId3 : "hello" +> : ^^^^^^^ +>'hello' : "hello" +> : ^^^^^^^ + +target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK +>target = { ...(cond ? { parentId: parentId3 } : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? { parentId: parentId3 } : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^ +>(cond ? { parentId: parentId3 } : {}) : { parentId: string; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond ? { parentId: parentId3 } : {} : { parentId: string; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond : boolean +> : ^^^^^^^ +>{ parentId: parentId3 } : { parentId: string; } +> : ^^^^^^^^^^^^^^^^^^^^^ +>parentId : string +> : ^^^^^^ +>parentId3 : "hello" +> : ^^^^^^^ +>{} : {} +> : ^^ + +// Spreading an object where the property is narrowed to string +if (cond && requestBody.parentId !== undefined) { +>cond && requestBody.parentId !== undefined : boolean +> : ^^^^^^^ +>cond : boolean +> : ^^^^^^^ +>requestBody.parentId !== undefined : boolean +> : ^^^^^^^ +>requestBody.parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>requestBody : Foo +> : ^^^ +>parentId : string | undefined +> : ^^^^^^^^^^^^^^^^^^ +>undefined : undefined +> : ^^^^^^^^^ + + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string +>target = { ...(cond ? { parentId: requestBody.parentId } : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^ +>target : Foo +> : ^^^ +>{ ...(cond ? { parentId: requestBody.parentId } : {}) } : { parentId?: string; } +> : ^^^^^^^^^^^^^^^^^^^^^^ +>(cond ? { parentId: requestBody.parentId } : {}) : { parentId: string; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond ? { parentId: requestBody.parentId } : {} : { parentId: string; } | {} +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>cond : true +> : ^^^^ +>{ parentId: requestBody.parentId } : { parentId: string; } +> : ^^^^^^^^^^^^^^^^^^^^^ +>parentId : string +> : ^^^^^^ +>requestBody.parentId : string +> : ^^^^^^ +>requestBody : Foo +> : ^^^ +>parentId : string +> : ^^^^^^ +>{} : {} +> : ^^ +} + diff --git a/tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts b/tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts new file mode 100644 index 0000000000000..00411a67985c8 --- /dev/null +++ b/tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts @@ -0,0 +1,42 @@ +// @target: es2015 +// @strict: true +// @exactOptionalPropertyTypes: true + + +type Foo = { + parentId?: string; +}; + +declare const requestBody: Foo; +declare const cond: boolean; +let target: Foo; + +// Direct assignment — correctly flagged +target = { parentId: requestBody.parentId }; // Error + +// Spread + ternary with optional property access — must also be flagged +target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error + +// Destructured optional property — must also be flagged +const { parentId } = requestBody; +target = { ...(cond ? { parentId } : {}) }; // Error + +// Explicit `string | undefined` value — must be flagged (was already working) +const parentId2 = '' as string | undefined; +target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error + +// ------------------------------------------------------------------ +// Valid cases — must NOT produce errors +// ------------------------------------------------------------------ + +// Spreading the whole Foo object is fine (its optional properties are already correctly typed) +target = { ...(cond ? requestBody : {}) }; // OK + +// A required string value is fine +const parentId3 = 'hello'; +target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK + +// Spreading an object where the property is narrowed to string +if (cond && requestBody.parentId !== undefined) { + target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string +}