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 to A ... A and A ... A is equivalent to {} ... A.
- Commutativity:
A ... B is not equivalent to B ... A. Properties of B overwrite properties of A with the same name in A ... B.
- Associativity:
(A ... B) ... C is equivalent to A ... (B ... C). ... is right-associative.
- Distributivity: Spread is distributive over
|, so A ... (B | C) is equivalent to A ... B | A ... C.
Assignment compatibility
A ... B is assignable to X if the properties and index signatures of A ... B are assignable to those of X, and X has no call or construct signatures.
X is assignable to A ... B if the properties and index signatures of X are assignable to those of A ... 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 property P or B has a property P, and
- Either
A.P or B.P is not a method.
In this case (A ... B).P has the type
- Of
B.P if B.P is not optional.
- Of
A.P | B.P if B.P is optional and A has a property P.
- 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 to A because it is missing call and construct signatures.
rest(rest(A)) is equivalent to rest(A).
rest(rest(A, a), b) is equivalent to rest(rest(A, b), a) and rest(A, a, b).
rest(A | B, a) is equivalent to rest(A, a) | rest(B, a).
Assignment compatibility
rest(T, x) is not assignable to T.
T is assignable to rest(T, x) because T 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.
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 syntaxspread1 ... spread2.{ ...spread }becomes{} ... spread.{ a, b, c, ...d}becomes{a, b, c} ... d{ a, b, c, ...d, ...e, f, g}becomes{a, b, c} ... d ... e ... { f, g }.Type Relationships
A ... A ... Ais equivalent toA ... AandA ... Ais equivalent to{} ... A.A ... Bis not equivalent toB ... A. Properties ofBoverwrite properties ofAwith the same name inA ... B.(A ... B) ... Cis equivalent toA ... (B ... C)....is right-associative.|, soA ... (B | C)is equivalent toA ... B | A ... C.Assignment compatibility
A ... Bis assignable toXif the properties and index signatures ofA ... Bare assignable to those ofX, andXhas no call or construct signatures.Xis assignable toA ... Bif the properties and index signatures ofXare 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 ... Bhas a propertyPifAhas a propertyPorBhas a propertyP, andA.PorB.Pis not a method.In this case
(A ... B).Phas the typeB.PifB.Pis not optional.A.P | B.PifB.Pis optional andAhas a propertyP.A.Potherwise.private,protectedandreadonlybehave the same way as optionality except that ifA.PorB.Pisprivate,protectedorreadonly, then(A ...B).Pisprivate,protectedorreadonly, respectively.Index signatures
The type
A ... Bhas an index signature ifAhas an index signature andBhas an index signature. The index signature's type is the union of the two index signatures' types.Call and Construct signatures
A ... Bhas 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)
Merging Two Objects
Overriding Properties
Default Properties
Multiple Merges
Getters on the Object Initializer
Getters in the Spread Object
Setters Are Not Executed When They're Redefined
Null/Undefined Are Ignored
Updating Deep Immutable Object
Note: If
A = { name: string, address: { address, zipCode: string }, items: { title: string }[] }, then the type of newVersion is equivalent toARest 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 typeTafter the propertiesa,bandchave been removed, as well as call signatures and construct signatures.A short example illustrates the way this type is used:
Type Relationships
rest(A)is not equivalent toAbecause 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.Tis assignable torest(T, x)becauseThas more properties and signatures.Properties and index signatures
The type
rest(A, P)removesPfromAif 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.