Skip to content

Object spread#11150

Merged
sandersn merged 103 commits intomasterfrom
object-spread
Nov 10, 2016
Merged

Object spread#11150
sandersn merged 103 commits intomasterfrom
object-spread

Conversation

@sandersn
Copy link
Copy Markdown
Member

@sandersn sandersn commented Sep 26, 2016

Leaves #10727 still open
Fixes #2103
Leaves #11100 still open

Important Update

The PR no longer contains a spread type, only spread syntax, which creates object types. I expect a spread type or something similar to be available in future versions of TypeScript.

Updates to proposed semantics: type variable assignability

A spread type containing spread elements whose types are type variables is assignable to another spread type also containing type variables if the non-type-variable properties and index signatures are assignable as above and the the type variables are the same variables in the same order.

For example: { a: number, ...T, b: number, ...U, ...x } is assignable to { b: number, a: number, ...x ...T, ...U} and vice versa. But the similar { a: number, ...T, b: number, ...U, ...x } is not assignable to { b: number, a: number, ...x ...U, ...T} because the order of T and U is reversed.

This rule is not strictly safe, since a type parameter could override any properties that precede it (see the rule for private below), but it would be odd if { ...T, ...U } were not assignable to { ...T, ...U }. Instead, this rule assumes that the contents of a type variable will not conflict with other properties.

Important Implementation Details

Spread types use a binary representation to make recursive transformations easier. This matches the proposal but the syntax remains close to spread's expression syntax.

getSpreadType creates a spread only if the spread type contains a type parameter. Otherwise it will create an anonymous type, or union or intersection of anonymous types. The binary structure is left deep, so the left side of a spread type is either another spread (the recursive case) or an object type (the terminal case). The right side of spread type is either a type parameter or an object type. Adjacent object types are spread into a single object type, so the structure will never have two adjacent object types, only adjacent type parameters.

Updated proposal

The spread part of #10727's updated proposal follows for convenience:

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 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 { ...spread } syntax to binary syntax spread1 ... spread2.

  1. { ...spread } becomes {} ... spread.
  2. { a, b, c, ...d} becomes {a, b, c} ... d
  3. 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 is equivalent to the properties of A. 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.
  • Associativity: (A ... B) ... C is equivalent to A ... (B ... C). ... is right-associative.
  • Distributivity: Spread is distribute over both | and &, so A ... (B | C) is equivalent to A ... B | A ... C and 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 variables

A spread type containing type parameters is assignable to another spread type if the the non-type-variable properties and index signatures are assignable as above and the type variables are the same variables in the same order.

For example: { a: number, b: number } ... T is assignable to { a: number } ...T and to T ... { a: number }. But the similar T ... U is not assignable to U ... T because the order of T and U is reversed.

Type inference

Type inference is allowed between an object type source and a spread type target with a type parameter as either the left or right side. If the type parameter is on the right, then its inferred type is the whole source type. If the type parameter is on the left, then its inferred type is the source type minus properties on the right side of the spread.

For example, if the target is T ... { a: number } and the source is { a: number, b: string }, the inferred type for T is { b: string }. Because spread types remove call and construct signatures, if the source were { a: number, b: string, (): void }, the type inferred for T would still be { b: string }.

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. In this case (A ... B).P has the type

  1. Of B.P if B.P is not optional.
  2. Of A.P | B.P if B.P is optional and A has a property P.
  3. Of A.P otherwise.

Index signatures spread the same way as properties.

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.

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

Loading
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support proposed ES Rest/Spread properties