Skip to content

Commit b968922

Browse files
authored
When calculating spreads, merge empty object into nonempty object to … (microsoft#34853)
* When calculating spreads, merge empty object into nonempty object to produce partial object to reduce complexity * Actually accept remainder of baselines * Limit simplification to single prop or partial types
1 parent 58a05f3 commit b968922

11 files changed

+1070
-42
lines changed

src/compiler/checker.ts

+63
Original file line numberDiff line numberDiff line change
@@ -12524,6 +12524,61 @@ namespace ts {
1252412524
return !!(type.flags & TypeFlags.Object) && !isGenericMappedType(type);
1252512525
}
1252612526

12527+
function isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: Type) {
12528+
return isEmptyObjectType(type) || !!(type.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index));
12529+
}
12530+
12531+
function isSinglePropertyAnonymousObjectType(type: Type) {
12532+
return !!(type.flags & TypeFlags.Object) &&
12533+
!!(getObjectFlags(type) & ObjectFlags.Anonymous) &&
12534+
(length(getPropertiesOfType(type)) === 1 || every(getPropertiesOfType(type), p => !!(p.flags & SymbolFlags.Optional)));
12535+
}
12536+
12537+
function tryMergeUnionOfObjectTypeAndEmptyObject(type: UnionType, readonly: boolean): Type | undefined {
12538+
if (type.types.length === 2) {
12539+
const firstType = type.types[0];
12540+
const secondType = type.types[1];
12541+
if (every(type.types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) {
12542+
return isEmptyObjectType(firstType) ? firstType : isEmptyObjectType(secondType) ? secondType : emptyObjectType;
12543+
}
12544+
if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(firstType) && isSinglePropertyAnonymousObjectType(secondType)) {
12545+
return getAnonymousPartialType(secondType);
12546+
}
12547+
if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(secondType) && isSinglePropertyAnonymousObjectType(firstType)) {
12548+
return getAnonymousPartialType(firstType);
12549+
}
12550+
}
12551+
12552+
function getAnonymousPartialType(type: Type) {
12553+
// gets the type as if it had been spread, but where everything in the spread is made optional
12554+
const members = createSymbolTable();
12555+
for (const prop of getPropertiesOfType(type)) {
12556+
if (getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) {
12557+
// do nothing, skip privates
12558+
}
12559+
else if (isSpreadableProperty(prop)) {
12560+
const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor);
12561+
const flags = SymbolFlags.Property | SymbolFlags.Optional;
12562+
const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0);
12563+
result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop);
12564+
result.declarations = prop.declarations;
12565+
result.nameType = prop.nameType;
12566+
result.syntheticOrigin = prop;
12567+
members.set(prop.escapedName, result);
12568+
}
12569+
}
12570+
const spread = createAnonymousType(
12571+
type.symbol,
12572+
members,
12573+
emptyArray,
12574+
emptyArray,
12575+
getIndexInfoOfType(type, IndexKind.String),
12576+
getIndexInfoOfType(type, IndexKind.Number));
12577+
spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
12578+
return spread;
12579+
}
12580+
}
12581+
1252712582
/**
1252812583
* Since the source of spread types are object literals, which are not binary,
1252912584
* this function should be called in a left folding style, with left = previous result of getSpreadType
@@ -12543,9 +12598,17 @@ namespace ts {
1254312598
return left;
1254412599
}
1254512600
if (left.flags & TypeFlags.Union) {
12601+
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(left as UnionType, readonly);
12602+
if (merged) {
12603+
return getSpreadType(merged, right, symbol, objectFlags, readonly);
12604+
}
1254612605
return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly));
1254712606
}
1254812607
if (right.flags & TypeFlags.Union) {
12608+
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(right as UnionType, readonly);
12609+
if (merged) {
12610+
return getSpreadType(left, merged, symbol, objectFlags, readonly);
12611+
}
1254912612
return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly));
1255012613
}
1255112614
if (right.flags & (TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)) {

tests/baselines/reference/objectSpread.types

+13-13
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ function from16326(this: { header: Header }, header: Header, authToken: string):
244244
>authToken : string
245245

246246
return {
247-
>{ ...this.header, ...header, ...authToken && { authToken } } : { head: string; body: string; authToken: string; } | { authToken: string; head: string; body: string; }
247+
>{ ...this.header, ...header, ...authToken && { authToken } } : { authToken: string; head: string; body: string; }
248248

249249
...this.header,
250250
>this.header : Header
@@ -277,9 +277,9 @@ function conditionalSpreadBoolean(b: boolean) : { x: number, y: number } {
277277
>13 : 13
278278

279279
o = {
280-
>o = { ...o, ...b && { x: 14 } } : { x: number; y: number; } | { x: number; y: number; }
280+
>o = { ...o, ...b && { x: 14 } } : { x: number; y: number; }
281281
>o : { x: number; y: number; }
282-
>{ ...o, ...b && { x: 14 } } : { x: number; y: number; } | { x: number; y: number; }
282+
>{ ...o, ...b && { x: 14 } } : { x: number; y: number; }
283283

284284
...o,
285285
>o : { x: number; y: number; }
@@ -292,8 +292,8 @@ function conditionalSpreadBoolean(b: boolean) : { x: number, y: number } {
292292
>14 : 14
293293
}
294294
let o2 = { ...b && { x: 21 }}
295-
>o2 : {}
296-
>{ ...b && { x: 21 }} : { x: number; } | {}
295+
>o2 : { x?: number; }
296+
>{ ...b && { x: 21 }} : { x?: number; }
297297
>b && { x: 21 } : false | { x: number; }
298298
>b : boolean
299299
>{ x: 21 } : { x: number; }
@@ -318,9 +318,9 @@ function conditionalSpreadNumber(nt: number): { x: number, y: number } {
318318
>16 : 16
319319

320320
o = {
321-
>o = { ...o, ...nt && { x: nt } } : { x: number; y: number; } | { x: number; y: number; }
321+
>o = { ...o, ...nt && { x: nt } } : { x: number; y: number; }
322322
>o : { x: number; y: number; }
323-
>{ ...o, ...nt && { x: nt } } : { x: number; y: number; } | { x: number; y: number; }
323+
>{ ...o, ...nt && { x: nt } } : { x: number; y: number; }
324324

325325
...o,
326326
>o : { x: number; y: number; }
@@ -333,8 +333,8 @@ function conditionalSpreadNumber(nt: number): { x: number, y: number } {
333333
>nt : number
334334
}
335335
let o2 = { ...nt && { x: nt }}
336-
>o2 : {}
337-
>{ ...nt && { x: nt }} : { x: number; } | {}
336+
>o2 : { x?: number; }
337+
>{ ...nt && { x: nt }} : { x?: number; }
338338
>nt && { x: nt } : 0 | { x: number; }
339339
>nt : number
340340
>{ x: nt } : { x: number; }
@@ -359,9 +359,9 @@ function conditionalSpreadString(st: string): { x: string, y: number } {
359359
>17 : 17
360360

361361
o = {
362-
>o = { ...o, ...st && { x: st } } : { x: string; y: number; } | { x: string; y: number; }
362+
>o = { ...o, ...st && { x: st } } : { x: string; y: number; }
363363
>o : { x: string; y: number; }
364-
>{ ...o, ...st && { x: st } } : { x: string; y: number; } | { x: string; y: number; }
364+
>{ ...o, ...st && { x: st } } : { x: string; y: number; }
365365

366366
...o,
367367
>o : { x: string; y: number; }
@@ -374,8 +374,8 @@ function conditionalSpreadString(st: string): { x: string, y: number } {
374374
>st : string
375375
}
376376
let o2 = { ...st && { x: st }}
377-
>o2 : {}
378-
>{ ...st && { x: st }} : { x: string; } | {}
377+
>o2 : { x?: string; }
378+
>{ ...st && { x: st }} : { x?: string; }
379379
>st && { x: st } : "" | { x: string; }
380380
>st : string
381381
>{ x: st } : { x: string; }

tests/baselines/reference/objectSpreadIndexSignature.types

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ declare const b: boolean;
4242
>b : boolean
4343

4444
indexed3 = { ...b ? indexed3 : undefined };
45-
>indexed3 = { ...b ? indexed3 : undefined } : {} | { [n: string]: number; }
45+
>indexed3 = { ...b ? indexed3 : undefined } : { [n: string]: number; }
4646
>indexed3 : { [n: string]: number; }
47-
>{ ...b ? indexed3 : undefined } : {} | { [n: string]: number; }
47+
>{ ...b ? indexed3 : undefined } : { [n: string]: number; }
4848
>b ? indexed3 : undefined : { [n: string]: number; } | undefined
4949
>b : boolean
5050
>indexed3 : { [n: string]: number; }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//// [objectSpreadRepeatedNullCheckPerf.ts]
2+
interface Props {
3+
readonly a?: string
4+
readonly b?: string
5+
readonly c?: string
6+
readonly d?: string
7+
readonly e?: string
8+
readonly f?: string
9+
readonly g?: string
10+
readonly h?: string
11+
readonly i?: string
12+
readonly j?: string
13+
readonly k?: string
14+
readonly l?: string
15+
readonly m?: string
16+
readonly n?: string
17+
readonly o?: string
18+
readonly p?: string
19+
readonly q?: string
20+
readonly r?: string
21+
readonly s?: string
22+
readonly t?: string
23+
readonly u?: string
24+
readonly v?: string
25+
readonly w?: string
26+
readonly x?: string
27+
readonly y?: string
28+
readonly z?: string
29+
}
30+
31+
function parseWithSpread(config: Record<string, number>): Props {
32+
return {
33+
...config.a !== undefined && { a: config.a.toString() },
34+
...config.b !== undefined && { b: config.b.toString() },
35+
...config.c !== undefined && { c: config.c.toString() },
36+
...config.d !== undefined && { d: config.d.toString() },
37+
...config.e !== undefined && { e: config.e.toString() },
38+
...config.f !== undefined && { f: config.f.toString() },
39+
...config.g !== undefined && { g: config.g.toString() },
40+
...config.h !== undefined && { h: config.h.toString() },
41+
...config.i !== undefined && { i: config.i.toString() },
42+
...config.j !== undefined && { j: config.j.toString() },
43+
...config.k !== undefined && { k: config.k.toString() },
44+
...config.l !== undefined && { l: config.l.toString() },
45+
...config.m !== undefined && { m: config.m.toString() },
46+
...config.n !== undefined && { n: config.n.toString() },
47+
...config.o !== undefined && { o: config.o.toString() },
48+
...config.p !== undefined && { p: config.p.toString() },
49+
...config.q !== undefined && { q: config.q.toString() },
50+
...config.r !== undefined && { r: config.r.toString() },
51+
...config.s !== undefined && { s: config.s.toString() },
52+
...config.t !== undefined && { t: config.t.toString() },
53+
...config.u !== undefined && { u: config.u.toString() },
54+
...config.v !== undefined && { v: config.v.toString() },
55+
...config.w !== undefined && { w: config.w.toString() },
56+
...config.x !== undefined && { x: config.x.toString() },
57+
...config.y !== undefined && { y: config.y.toString() },
58+
...config.z !== undefined && { z: config.z.toString() }
59+
}
60+
}
61+
62+
parseWithSpread({ a: 1, b: 2, z: 26 })
63+
64+
//// [objectSpreadRepeatedNullCheckPerf.js]
65+
"use strict";
66+
var __assign = (this && this.__assign) || function () {
67+
__assign = Object.assign || function(t) {
68+
for (var s, i = 1, n = arguments.length; i < n; i++) {
69+
s = arguments[i];
70+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
71+
t[p] = s[p];
72+
}
73+
return t;
74+
};
75+
return __assign.apply(this, arguments);
76+
};
77+
function parseWithSpread(config) {
78+
return __assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign(__assign({}, config.a !== undefined && { a: config.a.toString() }), config.b !== undefined && { b: config.b.toString() }), config.c !== undefined && { c: config.c.toString() }), config.d !== undefined && { d: config.d.toString() }), config.e !== undefined && { e: config.e.toString() }), config.f !== undefined && { f: config.f.toString() }), config.g !== undefined && { g: config.g.toString() }), config.h !== undefined && { h: config.h.toString() }), config.i !== undefined && { i: config.i.toString() }), config.j !== undefined && { j: config.j.toString() }), config.k !== undefined && { k: config.k.toString() }), config.l !== undefined && { l: config.l.toString() }), config.m !== undefined && { m: config.m.toString() }), config.n !== undefined && { n: config.n.toString() }), config.o !== undefined && { o: config.o.toString() }), config.p !== undefined && { p: config.p.toString() }), config.q !== undefined && { q: config.q.toString() }), config.r !== undefined && { r: config.r.toString() }), config.s !== undefined && { s: config.s.toString() }), config.t !== undefined && { t: config.t.toString() }), config.u !== undefined && { u: config.u.toString() }), config.v !== undefined && { v: config.v.toString() }), config.w !== undefined && { w: config.w.toString() }), config.x !== undefined && { x: config.x.toString() }), config.y !== undefined && { y: config.y.toString() }), config.z !== undefined && { z: config.z.toString() });
79+
}
80+
parseWithSpread({ a: 1, b: 2, z: 26 });

0 commit comments

Comments
 (0)