Description
openedon Sep 6, 2016
The spread type is a new type operator that types the TC39 stage 3 object spread operator. Its counterpart, the difference type, will type the proposed object rest destructuring operator. The spread type { ...A, ...B }
combines the properties, but not the call or construct signatures, of entities A and B.
The pull request is at #11150. The original issue for spread/rest types is #2103. Note that this proposal deviates from the specification by keeping all properties except methods, not just own enumerable ones.
Proposal syntax
The type syntax in this proposal differs from the type syntax as implemented in order to treat spread as a binary operator. Three rules are needed to convert the { ...spread1, ...spread2 }
syntax to binary syntax spread1 ... spread2
.
{ ...spread }
becomes{} ... spread
.{ a, b, c, ...d}
becomes{a, b, c} ... d
- Multiple spreads inside an object literal are treated as sequences of binary spreads:
{ a, b, c, ...d, ...e, f, g}
becomes{a, b, c} ... d ... e ... { f, g }
.
Type Relationships
- Identity:
A ... A ... A
is equivalent toA ... A
andA ... A
is equivalent to{} ... A
. - Commutativity:
A ... B
is not equivalent toB ... A
. Properties ofB
overwrite properties ofA
with the same name inA ... B
. - Associativity:
(A ... B) ... C
is equivalent toA ... (B ... C)
....
is right-associative. - Distributivity: Spread is distributive over
|
, soA ... (B | C)
is equivalent toA ... B | A ... C
.
Assignment compatibility
A ... B
is assignable toX
if the properties and index signatures ofA ... B
are assignable to those ofX
, andX
has no call or construct signatures.X
is assignable toA ... B
if the properties and index signatures ofX
are assignable to those ofA ... B
.
Type parameters
A spread type containing type parameters is assignable to another spread type if the type if the source and target types are both of the form T ... { some, object, type }
and both source and target have the same type parameter and the source object type is assignable to the target object type.
Type inference
Spread types are not type inference targets.
Properties and index signatures
In the following definitions, 'property' means either a property or a get accessor.
The type A ... B
has a property P
if
A
has a propertyP
orB
has a propertyP
, and- Either
A.P
orB.P
is not a method.
In this case (A ... B).P
has the type
- Of
B.P
ifB.P
is not optional. - Of
A.P | B.P
ifB.P
is optional andA
has a propertyP
. - Of
A.P
otherwise.
private
, protected
and readonly
behave the same way as optionality except that if A.P
or B.P
is private
, protected
or readonly
, then (A ...B).P
is private
, protected
or readonly
, respectively.
Index signatures
The type A ... B
has an index signature if A
has an index signature and B
has an index signature. The index signature's type is the union of the two index signatures' types.
Call and Construct signatures
A ... B
has no call signatures and no construct signatures, since these are not properties.
Precedence
Precedence of ...
is higher than &
and |
. Since the language syntax is that of object type literals, precedence doesn't matter since the braces act as boundaries of the spread type.
Examples
Taken from the TC39 proposal and given types.
Shallow Clone (excluding prototype)
let aClone: { ...A } = { ...a };
Merging Two Objects
let ab: { ...A, ...B } = { ...a, ...b };
Overriding Properties
let aWithOverrides: { ...A, x: number, y: number } = { ...a, x: 1, y: 2 };
// equivalent to
let aWithOverrides: { ...A, ...{ x: number, y: number } } = { ...a, ...{ x: 1, y: 2 } };
Default Properties
let aWithDefaults: { x: number, y: number, ...A } = { x: 1, y: 2, ...a };
Multiple Merges
// Note: getters on a are executed twice
let xyWithAandB: { x: number, ...A, y: number, ...B, ...A } = { x: 1, ...a, y: 2, ...b, ...a };
// equivalent to
let xyWithAandB: { x: number, y: number, ...B, ...A } = { x: 1, ...a, y: 2, ...b, ...a };
Getters on the Object Initializer
// Does not throw because .x isn't evaluated yet. It's defined.
let aWithXGetter: { ...A, x: never } = { ...a, get x() { throw new Error('not thrown yet') } };
Getters in the Spread Object
// Throws because the .x property of the inner object is evaluated when the
// property value is copied over to the surrounding object initializer.
let runtimeError: { ...A, x: never } = { ...a, ...{ get x() { throw new Error('thrown now') } } };
Setters Are Not Executed When They're Redefined
let z: { x: number } = { set x() { throw new Error(); }, ...{ x: 1 } }; // No error
Null/Undefined Are Ignored
let emptyObject: {} = { ...null, ...undefined }; // no runtime error
Updating Deep Immutable Object
let newVersion: { ...A, name: string, address: { address, zipCode: string }, items: { title: string }[] } = {
...previousVersion,
name: 'New Name', // Override the name property
address: { ...previousVersion.address, zipCode: '99999' } // Update nested zip code
items: [...previousVersion.items, { title: 'New Item' }] // Add an item to the list of items
};
Note: If A = { name: string, address: { address, zipCode: string }, items: { title: string }[] }
, then the type of newVersion is equivalent to A
Rest types
The difference type is the opposite of the spread type. It types the TC39 stage 3 object-rest destructuring operator. The difference type rest(T, a, b, c)
represents the type T
after the properties a
, b
and c
have been removed, as well as call signatures and construct signatures.
A short example illustrates the way this type is used:
/** JavaScript version */
function removeX(o) {
let { x, ...rest } = o;
return rest;
}
/** Typescript version */
function removeX<T extends { x: number, y: number }>(o: T): rest(T, x) {
let { x, ...rest }: T = o;
return rest;
}
Type Relationships
rest(A)
is not equivalent toA
because it is missing call and construct signatures.rest(rest(A))
is equivalent torest(A)
.rest(rest(A, a), b)
is equivalent torest(rest(A, b), a)
andrest(A, a, b)
.rest(A | B, a)
is equivalent torest(A, a) | rest(B, a)
.
Assignment compatibility
rest(T, x)
is not assignable toT
.T
is assignable torest(T, x)
becauseT
has more properties and signatures.
Properties and index signatures
The type rest(A, P)
removes P
from A
if it exists. Otherwise, it does nothing.
Call and Construct signatures
rest(A)
does not have call or construct signatures.
Precedence
Difference types have similar precedence to -
in the expression grammar, particularly compared to &
and |
. TODO: Find out what this precedence is.