-
Notifications
You must be signed in to change notification settings - Fork 12.8k
When calculating spreads, merge empty object into nonempty object to … #34853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
25da0a9
8957dfe
7181c2a
7be8c79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12514,6 +12514,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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why can't we construct a real partial type here? Probably because it would just be expensive and might not exist, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just doing the spread operation (skipping privates, only taking spreadable props) here, but also mixing in the |
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should isSpreadableProperty exclude private/protected instead of needing a separate check? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope - this is pretty much a clone of the property copying code farther down in |
||
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 | ||
|
@@ -12533,9 +12588,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)) { | ||
|
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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why only cover a pair of types? is that just the most common case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty much.
{} | Whatever
being roughly equivalent toPartial<whatever>
is fairly obvious -{} | Foo | Bar
is... less obvious?