Skip to content

Commit

Permalink
When calculating spreads, merge empty object into nonempty object to … (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
weswigham committed Nov 23, 2019
1 parent 58a05f3 commit b968922
Show file tree
Hide file tree
Showing 11 changed files with 1,070 additions and 42 deletions.
63 changes: 63 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12524,6 +12524,61 @@ namespace ts {
return !!(type.flags & TypeFlags.Object) && !isGenericMappedType(type);
}

function isEmptyObjectTypeOrSpreadsIntoEmptyObject(type: Type) {
return isEmptyObjectType(type) || !!(type.flags & (TypeFlags.Null | TypeFlags.Undefined | TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index));
}

function isSinglePropertyAnonymousObjectType(type: Type) {
return !!(type.flags & TypeFlags.Object) &&
!!(getObjectFlags(type) & ObjectFlags.Anonymous) &&
(length(getPropertiesOfType(type)) === 1 || every(getPropertiesOfType(type), p => !!(p.flags & SymbolFlags.Optional)));
}

function tryMergeUnionOfObjectTypeAndEmptyObject(type: UnionType, readonly: boolean): Type | undefined {
if (type.types.length === 2) {
const firstType = type.types[0];
const secondType = type.types[1];
if (every(type.types, isEmptyObjectTypeOrSpreadsIntoEmptyObject)) {
return isEmptyObjectType(firstType) ? firstType : isEmptyObjectType(secondType) ? secondType : emptyObjectType;
}
if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(firstType) && isSinglePropertyAnonymousObjectType(secondType)) {
return getAnonymousPartialType(secondType);
}
if (isEmptyObjectTypeOrSpreadsIntoEmptyObject(secondType) && isSinglePropertyAnonymousObjectType(firstType)) {
return getAnonymousPartialType(firstType);
}
}

function getAnonymousPartialType(type: Type) {
// gets the type as if it had been spread, but where everything in the spread is made optional
const members = createSymbolTable();
for (const prop of getPropertiesOfType(type)) {
if (getDeclarationModifierFlagsFromSymbol(prop) & (ModifierFlags.Private | ModifierFlags.Protected)) {
// do nothing, skip privates
}
else if (isSpreadableProperty(prop)) {
const isSetonlyAccessor = prop.flags & SymbolFlags.SetAccessor && !(prop.flags & SymbolFlags.GetAccessor);
const flags = SymbolFlags.Property | SymbolFlags.Optional;
const result = createSymbol(flags, prop.escapedName, readonly ? CheckFlags.Readonly : 0);
result.type = isSetonlyAccessor ? undefinedType : getTypeOfSymbol(prop);
result.declarations = prop.declarations;
result.nameType = prop.nameType;
result.syntheticOrigin = prop;
members.set(prop.escapedName, result);
}
}
const spread = createAnonymousType(
type.symbol,
members,
emptyArray,
emptyArray,
getIndexInfoOfType(type, IndexKind.String),
getIndexInfoOfType(type, IndexKind.Number));
spread.objectFlags |= ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
return spread;
}
}

/**
* Since the source of spread types are object literals, which are not binary,
* this function should be called in a left folding style, with left = previous result of getSpreadType
Expand All @@ -12543,9 +12598,17 @@ namespace ts {
return left;
}
if (left.flags & TypeFlags.Union) {
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(left as UnionType, readonly);
if (merged) {
return getSpreadType(merged, right, symbol, objectFlags, readonly);
}
return mapType(left, t => getSpreadType(t, right, symbol, objectFlags, readonly));
}
if (right.flags & TypeFlags.Union) {
const merged = tryMergeUnionOfObjectTypeAndEmptyObject(right as UnionType, readonly);
if (merged) {
return getSpreadType(left, merged, symbol, objectFlags, readonly);
}
return mapType(right, t => getSpreadType(left, t, symbol, objectFlags, readonly));
}
if (right.flags & (TypeFlags.BooleanLike | TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.EnumLike | TypeFlags.NonPrimitive | TypeFlags.Index)) {
Expand Down
26 changes: 13 additions & 13 deletions tests/baselines/reference/objectSpread.types
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ function from16326(this: { header: Header }, header: Header, authToken: string):
>authToken : string

return {
>{ ...this.header, ...header, ...authToken && { authToken } } : { head: string; body: string; authToken: string; } | { authToken: string; head: string; body: string; }
>{ ...this.header, ...header, ...authToken && { authToken } } : { authToken: string; head: string; body: string; }

...this.header,
>this.header : Header
Expand Down Expand Up @@ -277,9 +277,9 @@ function conditionalSpreadBoolean(b: boolean) : { x: number, y: number } {
>13 : 13

o = {
>o = { ...o, ...b && { x: 14 } } : { x: number; y: number; } | { x: number; y: number; }
>o = { ...o, ...b && { x: 14 } } : { x: number; y: number; }
>o : { x: number; y: number; }
>{ ...o, ...b && { x: 14 } } : { x: number; y: number; } | { x: number; y: number; }
>{ ...o, ...b && { x: 14 } } : { x: number; y: number; }

...o,
>o : { x: number; y: number; }
Expand All @@ -292,8 +292,8 @@ function conditionalSpreadBoolean(b: boolean) : { x: number, y: number } {
>14 : 14
}
let o2 = { ...b && { x: 21 }}
>o2 : {}
>{ ...b && { x: 21 }} : { x: number; } | {}
>o2 : { x?: number; }
>{ ...b && { x: 21 }} : { x?: number; }
>b && { x: 21 } : false | { x: number; }
>b : boolean
>{ x: 21 } : { x: number; }
Expand All @@ -318,9 +318,9 @@ function conditionalSpreadNumber(nt: number): { x: number, y: number } {
>16 : 16

o = {
>o = { ...o, ...nt && { x: nt } } : { x: number; y: number; } | { x: number; y: number; }
>o = { ...o, ...nt && { x: nt } } : { x: number; y: number; }
>o : { x: number; y: number; }
>{ ...o, ...nt && { x: nt } } : { x: number; y: number; } | { x: number; y: number; }
>{ ...o, ...nt && { x: nt } } : { x: number; y: number; }

...o,
>o : { x: number; y: number; }
Expand All @@ -333,8 +333,8 @@ function conditionalSpreadNumber(nt: number): { x: number, y: number } {
>nt : number
}
let o2 = { ...nt && { x: nt }}
>o2 : {}
>{ ...nt && { x: nt }} : { x: number; } | {}
>o2 : { x?: number; }
>{ ...nt && { x: nt }} : { x?: number; }
>nt && { x: nt } : 0 | { x: number; }
>nt : number
>{ x: nt } : { x: number; }
Expand All @@ -359,9 +359,9 @@ function conditionalSpreadString(st: string): { x: string, y: number } {
>17 : 17

o = {
>o = { ...o, ...st && { x: st } } : { x: string; y: number; } | { x: string; y: number; }
>o = { ...o, ...st && { x: st } } : { x: string; y: number; }
>o : { x: string; y: number; }
>{ ...o, ...st && { x: st } } : { x: string; y: number; } | { x: string; y: number; }
>{ ...o, ...st && { x: st } } : { x: string; y: number; }

...o,
>o : { x: string; y: number; }
Expand All @@ -374,8 +374,8 @@ function conditionalSpreadString(st: string): { x: string, y: number } {
>st : string
}
let o2 = { ...st && { x: st }}
>o2 : {}
>{ ...st && { x: st }} : { x: string; } | {}
>o2 : { x?: string; }
>{ ...st && { x: st }} : { x?: string; }
>st && { x: st } : "" | { x: string; }
>st : string
>{ x: st } : { x: string; }
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/objectSpreadIndexSignature.types
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ declare const b: boolean;
>b : boolean

indexed3 = { ...b ? indexed3 : undefined };
>indexed3 = { ...b ? indexed3 : undefined } : {} | { [n: string]: number; }
>indexed3 = { ...b ? indexed3 : undefined } : { [n: string]: number; }
>indexed3 : { [n: string]: number; }
>{ ...b ? indexed3 : undefined } : {} | { [n: string]: number; }
>{ ...b ? indexed3 : undefined } : { [n: string]: number; }
>b ? indexed3 : undefined : { [n: string]: number; } | undefined
>b : boolean
>indexed3 : { [n: string]: number; }
Expand Down
80 changes: 80 additions & 0 deletions tests/baselines/reference/objectSpreadRepeatedNullCheckPerf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//// [objectSpreadRepeatedNullCheckPerf.ts]
interface Props {
readonly a?: string
readonly b?: string
readonly c?: string
readonly d?: string
readonly e?: string
readonly f?: string
readonly g?: string
readonly h?: string
readonly i?: string
readonly j?: string
readonly k?: string
readonly l?: string
readonly m?: string
readonly n?: string
readonly o?: string
readonly p?: string
readonly q?: string
readonly r?: string
readonly s?: string
readonly t?: string
readonly u?: string
readonly v?: string
readonly w?: string
readonly x?: string
readonly y?: string
readonly z?: string
}

function parseWithSpread(config: Record<string, number>): Props {
return {
...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() }
}
}

parseWithSpread({ a: 1, b: 2, z: 26 })

//// [objectSpreadRepeatedNullCheckPerf.js]
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function parseWithSpread(config) {
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() });
}
parseWithSpread({ a: 1, b: 2, z: 26 });

0 comments on commit b968922

Please sign in to comment.