Skip to content

Commit d71cc12

Browse files
committed
Fix exactOptionalPropertyTypes not enforced through spread-with-ternary (#63240)
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.
1 parent 9059e5b commit d71cc12

6 files changed

Lines changed: 521 additions & 1 deletion

src/compiler/checker.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20135,7 +20135,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2013520135
const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor);
2013620136
const flags = SymbolFlags.Property | SymbolFlags.Optional;
2013720137
const result = createSymbol(flags, prop.escapedName, getIsLateCheckFlag(prop) | (readonly ? CheckFlags.Readonly : 0));
20138-
result.links.type = isSetonlyAccessor ? undefinedType : addOptionality(getTypeOfSymbol(prop), /*isProperty*/ true);
20138+
let propType = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop);
20139+
if (exactOptionalPropertyTypes && !isSetonlyAccessor && !(prop.flags & SymbolFlags.Optional) && containsMissingType(propType)) {
20140+
propType = getUnionType([removeMissingOrUndefinedType(propType), undefinedType]);
20141+
}
20142+
result.links.type = isSetonlyAccessor ? undefinedType : addOptionality(propType, /*isProperty*/ true);
2013920143
result.declarations = prop.declarations;
2014020144
result.links.nameType = getSymbolLinks(prop).nameType;
2014120145
result.links.syntheticOrigin = prop;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
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.
2+
Types of property 'parentId' are incompatible.
3+
Type 'string | undefined' is not assignable to type 'string'.
4+
Type 'undefined' is not assignable to type 'string'.
5+
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.
6+
Types of property 'parentId' are incompatible.
7+
Type 'string | undefined' is not assignable to type 'string'.
8+
Type 'undefined' is not assignable to type 'string'.
9+
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.
10+
Types of property 'parentId' are incompatible.
11+
Type 'string | undefined' is not assignable to type 'string'.
12+
Type 'undefined' is not assignable to type 'string'.
13+
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.
14+
Types of property 'parentId' are incompatible.
15+
Type 'string | undefined' is not assignable to type 'string'.
16+
Type 'undefined' is not assignable to type 'string'.
17+
18+
19+
==== exactOptionalPropertyTypes_spreadTernary.ts (4 errors) ====
20+
// Repro from https://github.com/microsoft/TypeScript/issues/63240
21+
// exactOptionalPropertyTypes should flag optional-property values spread via ternary
22+
23+
type Foo = {
24+
parentId?: string;
25+
};
26+
27+
declare const requestBody: Foo;
28+
declare const cond: boolean;
29+
let target: Foo;
30+
31+
// Direct assignment — correctly flagged
32+
target = { parentId: requestBody.parentId }; // Error
33+
~~~~~~
34+
!!! 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.
35+
!!! error TS2375: Types of property 'parentId' are incompatible.
36+
!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'.
37+
!!! error TS2375: Type 'undefined' is not assignable to type 'string'.
38+
39+
// Spread + ternary with optional property access — must also be flagged
40+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error
41+
~~~~~~
42+
!!! 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.
43+
!!! error TS2375: Types of property 'parentId' are incompatible.
44+
!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'.
45+
!!! error TS2375: Type 'undefined' is not assignable to type 'string'.
46+
47+
// Destructured optional property — must also be flagged
48+
const { parentId } = requestBody;
49+
target = { ...(cond ? { parentId } : {}) }; // Error
50+
~~~~~~
51+
!!! 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.
52+
!!! error TS2375: Types of property 'parentId' are incompatible.
53+
!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'.
54+
!!! error TS2375: Type 'undefined' is not assignable to type 'string'.
55+
56+
// Explicit `string | undefined` value — must be flagged (was already working)
57+
const parentId2 = '' as string | undefined;
58+
target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error
59+
~~~~~~
60+
!!! 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.
61+
!!! error TS2375: Types of property 'parentId' are incompatible.
62+
!!! error TS2375: Type 'string | undefined' is not assignable to type 'string'.
63+
!!! error TS2375: Type 'undefined' is not assignable to type 'string'.
64+
65+
// ------------------------------------------------------------------
66+
// Valid cases — must NOT produce errors
67+
// ------------------------------------------------------------------
68+
69+
// Spreading the whole Foo object is fine (its optional properties are already correctly typed)
70+
target = { ...(cond ? requestBody : {}) }; // OK
71+
72+
// A required string value is fine
73+
const parentId3 = 'hello';
74+
target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK
75+
76+
// Spreading an object where the property is narrowed to string
77+
if (cond && requestBody.parentId !== undefined) {
78+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string
79+
}
80+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//// [tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts] ////
2+
3+
//// [exactOptionalPropertyTypes_spreadTernary.ts]
4+
type Foo = {
5+
parentId?: string;
6+
};
7+
8+
declare const requestBody: Foo;
9+
declare const cond: boolean;
10+
let target: Foo;
11+
12+
// Direct assignment — correctly flagged
13+
target = { parentId: requestBody.parentId }; // Error
14+
15+
// Spread + ternary with optional property access — must also be flagged
16+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error
17+
18+
// Destructured optional property — must also be flagged
19+
const { parentId } = requestBody;
20+
target = { ...(cond ? { parentId } : {}) }; // Error
21+
22+
// Explicit `string | undefined` value — must be flagged (was already working)
23+
const parentId2 = '' as string | undefined;
24+
target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error
25+
26+
// ------------------------------------------------------------------
27+
// Valid cases — must NOT produce errors
28+
// ------------------------------------------------------------------
29+
30+
// Spreading the whole Foo object is fine (its optional properties are already correctly typed)
31+
target = { ...(cond ? requestBody : {}) }; // OK
32+
33+
// A required string value is fine
34+
const parentId3 = 'hello';
35+
target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK
36+
37+
// Spreading an object where the property is narrowed to string
38+
if (cond && requestBody.parentId !== undefined) {
39+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string
40+
}
41+
42+
43+
//// [exactOptionalPropertyTypes_spreadTernary.js]
44+
"use strict";
45+
// Repro from https://github.com/microsoft/TypeScript/issues/63240
46+
// exactOptionalPropertyTypes should flag optional-property values spread via ternary
47+
let target;
48+
// Direct assignment — correctly flagged
49+
target = { parentId: requestBody.parentId }; // Error
50+
// Spread + ternary with optional property access — must also be flagged
51+
target = Object.assign({}, (cond ? { parentId: requestBody.parentId } : {})); // Error
52+
// Destructured optional property — must also be flagged
53+
const { parentId } = requestBody;
54+
target = Object.assign({}, (cond ? { parentId } : {})); // Error
55+
// Explicit `string | undefined` value — must be flagged (was already working)
56+
const parentId2 = '';
57+
target = Object.assign({}, (cond ? { parentId: parentId2 } : {})); // Error
58+
// ------------------------------------------------------------------
59+
// Valid cases — must NOT produce errors
60+
// ------------------------------------------------------------------
61+
// Spreading the whole Foo object is fine (its optional properties are already correctly typed)
62+
target = Object.assign({}, (cond ? requestBody : {})); // OK
63+
// A required string value is fine
64+
const parentId3 = 'hello';
65+
target = Object.assign({}, (cond ? { parentId: parentId3 } : {})); // OK
66+
// Spreading an object where the property is narrowed to string
67+
if (cond && requestBody.parentId !== undefined) {
68+
target = Object.assign({}, (cond ? { parentId: requestBody.parentId } : {})); // OK — narrowed to string
69+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//// [tests/cases/compiler/exactOptionalPropertyTypes_spreadTernary.ts] ////
2+
3+
=== exactOptionalPropertyTypes_spreadTernary.ts ===
4+
// Repro from https://github.com/microsoft/TypeScript/issues/63240
5+
// exactOptionalPropertyTypes should flag optional-property values spread via ternary
6+
7+
type Foo = {
8+
>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0))
9+
10+
parentId?: string;
11+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
12+
13+
};
14+
15+
declare const requestBody: Foo;
16+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
17+
>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0))
18+
19+
declare const cond: boolean;
20+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
21+
22+
let target: Foo;
23+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
24+
>Foo : Symbol(Foo, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 0, 0))
25+
26+
// Direct assignment — correctly flagged
27+
target = { parentId: requestBody.parentId }; // Error
28+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
29+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 12, 10))
30+
>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
31+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
32+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
33+
34+
// Spread + ternary with optional property access — must also be flagged
35+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // Error
36+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
37+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
38+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 15, 23))
39+
>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
40+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
41+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
42+
43+
// Destructured optional property — must also be flagged
44+
const { parentId } = requestBody;
45+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 18, 7))
46+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
47+
48+
target = { ...(cond ? { parentId } : {}) }; // Error
49+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
50+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
51+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 19, 23))
52+
53+
// Explicit `string | undefined` value — must be flagged (was already working)
54+
const parentId2 = '' as string | undefined;
55+
>parentId2 : Symbol(parentId2, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 22, 5))
56+
57+
target = { ...(cond ? { parentId: parentId2 } : {}) }; // Error
58+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
59+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
60+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 23, 23))
61+
>parentId2 : Symbol(parentId2, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 22, 5))
62+
63+
// ------------------------------------------------------------------
64+
// Valid cases — must NOT produce errors
65+
// ------------------------------------------------------------------
66+
67+
// Spreading the whole Foo object is fine (its optional properties are already correctly typed)
68+
target = { ...(cond ? requestBody : {}) }; // OK
69+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
70+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
71+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
72+
73+
// A required string value is fine
74+
const parentId3 = 'hello';
75+
>parentId3 : Symbol(parentId3, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 33, 5))
76+
77+
target = { ...(cond ? { parentId: parentId3 } : {}) }; // OK
78+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
79+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
80+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 34, 23))
81+
>parentId3 : Symbol(parentId3, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 33, 5))
82+
83+
// Spreading an object where the property is narrowed to string
84+
if (cond && requestBody.parentId !== undefined) {
85+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
86+
>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
87+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
88+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
89+
>undefined : Symbol(undefined)
90+
91+
target = { ...(cond ? { parentId: requestBody.parentId } : {}) }; // OK — narrowed to string
92+
>target : Symbol(target, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 9, 3))
93+
>cond : Symbol(cond, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 8, 13))
94+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 38, 27))
95+
>requestBody.parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
96+
>requestBody : Symbol(requestBody, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 7, 13))
97+
>parentId : Symbol(parentId, Decl(exactOptionalPropertyTypes_spreadTernary.ts, 3, 12))
98+
}
99+

0 commit comments

Comments
 (0)