Repository navigation
Add spread/rest higher-order types operator #10727
Description
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 ... Ais equivalent toA ... AandA ... Ais equivalent to{} ... A. - Commutativity:
A ... Bis not equivalent toB ... A. Properties ofBoverwrite properties ofAwith the same name inA ... B. - Associativity:
(A ... B) ... Cis equivalent toA ... (B ... C)....is right-associative. - Distributivity: Spread is distributive over
|, 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 ... B has a property P if
Ahas a propertyPorBhas a propertyP, and- Either
A.PorB.Pis not a method.
In this case (A ... B).P has the type
- Of
B.PifB.Pis not optional. - Of
A.P | B.PifB.Pis optional andAhas a propertyP. - Of
A.Potherwise.
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 errorNull/Undefined Are Ignored
let emptyObject: {} = { ...null, ...undefined }; // no runtime errorUpdating 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 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) 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.
Activity
sandersn commentedon Sep 6, 2016
Anders Hejlsberg (@ahejlsberg) Ryan Cavanaugh (@RyanCavanaugh) can you take a look at the spread type section and see if there's something I forgot to take into account? The rest type section isn't really done because I want to talk about the problems in person.
mhegazy commentedon Sep 6, 2016
You mention "own" properties; the type system today does not have any definition for this, and you do not define it clearly. I would say we should just ignore this for now.
sandersn commentedon Sep 6, 2016
Defining 'own' as 'not inherited' gets us pretty close, since we have a good definition of inherited already. Actually, if I recall, the PR implements 'enumerable' as 'things that are not methods', which is pretty close in a similar way. There will still be false positives, but not from ES6-style class hierarchies.
mhegazy commentedon Sep 6, 2016
Interfaces are the issue. it is common for ppl to define interfaces as hierarchy to share declarations. so are these "own" or not?
sandersn commentedon Sep 6, 2016
I'm having trouble coming up with an example. Here's what I got so far.
IgetsbfromB, but in what circumstance wouldbnot be treated as an own property? In this example,Cdoesn't haveaORb, so neither does{ ... C }. But that's already how classes work.If you have an interface hierarchy that matches the class hierarchy, then inheritance still works as a definition for own types:
mhegazy commentedon Sep 6, 2016
I meant speading a value whose tupe is an interface:
sandersn commentedon Sep 6, 2016
Daniel Rosenwasser (@DanielRosenwasser) and I came up with some counterexamples for both own and enumerable properties. They are below. Basically, since we don't track this information in the type system, we need either
I'm not sure how hard (1) is.
The missing piece in this example is that
Cinstances are assignable toIbut lose the class inheritance information. Unfortunately this loss of information extends to enumerability as well:In this example,
Ispecifies a function property butCprovides a method, which is legal in TypeScript. Whencgets spread, though,fdrops out andi.f()will fail at runtime.sandersn commentedon Sep 7, 2016
I updated the proposal to not specify own, enumerable properties. I should add a note that we deviate from the stage 2 spec, though.
wclr commentedon Sep 11, 2016
Subtraction types would really be very nice to have.
saschanaz commentedon Sep 20, 2016
For the subtraction type example, shouldn't it be error?
sandersn commentedon Sep 20, 2016
Yes, thanks. I'll update the proposal.
felixfbecker commentedon Sep 26, 2016
This is pretty awesome! One thing that is not clear to me: What will spreading a class do? Will it take the instance properties or the static properties? I would say the first, so to get the static properties is spreading like
...(typeof MyClass)supported?The use case is as follows: methods/constructors that take an object literal as an argument, for example Sequelize (ORM):
What is limiting here of course still is that there is no way to mark all the properties as optional, but in the case of Sequelize all properties can be
undefinedbecause you can choose to not eager-load all attributes.It would also be nice to know if a union type of a spread type and an index signature type is allowed:
Regarding subtraction types, isn't a subtraction a mathematical operation on numbers? Shouldn't this really be the difference operator
\?sandersn commentedon Sep 26, 2016
You get the instance properties. That's because the spec says that you get the own properties, and instance properties are the closest concept that the compiler tracks.
It sounds like you would be interested in the
partialtype operator.You can add index signatures directly to your spread types:
The syntax is up in the air right now. The spread type PR implements an object-literal-like syntax but it's not final.
felixfbecker commentedon Sep 26, 2016
https://en.wikipedia.org/wiki/Complement_(set_theory)
What speaks against
\is that a lot of languages use-as an operator, not everyone is familiar with set theory, backslash has a meaning of "escaping" stuff and JavaScript is known for abusing arithmetic operators already (concatenation should really be., not+).But maybe we can at least not call it subtraction type, but difference type in the proposal (you wouldn't call a string concatenation a string addition, just because it uses the
+)sandersn commentedon Sep 27, 2016
(4) Good idea. Done.
99 remaining items
exsoflowe commentedon Feb 4, 2019
Is it related issues? Is it normal behavior?
Typescript Playground
MartinJohns commentedon Feb 4, 2019
exsoflowe This is expected behavior.
In the first case of
resultSpreadit does not throw an error because of theanytype. When you mark an object asanythen you lose all compiler type support. Spreading ananyobject results in the object becominganyitself. So you're creating ananyobject literal, then assign that to theBoxvariable. Assigninganyis possible to any other type.In the second case of
resultAssignit is the same issue when you keep the type asany. When you have an actual type, e.g.{ width: number; height: number }, then the type of of theObject.assign()result will be a combination of both provided types. In your example this will be:{ width: number; height: number }and{ title: string; anyField: number }becomes{ width: number; height: number; title: string; anyField: number }. This type is then assigned to aBoxvariable and the compatibility check happens. Your new type consisting of the four properties matches the interface (it has both properties ofBox) and can be assigned as a result.The excess-property-check only happens for object literals for known source and target types. In your
Object.assignexample it won't work becauseObject.assignis generic. The compiler can't infer a target type because it's a type argument, so the type of the object literal becomes the target type, it is notBox.And please note that the issue tracker is for discussion of TypeScript features and bugs. It is not for questions. Please use Stack Overflow or other resources for help writing TypeScript code.
craigkovatch commentedon Jun 29, 2019
Hi, I'm a bit out of my depth with advanced types, but think I'm hitting an error that I think this issue may cover. Please let me know if it does; if it doesn't, I will delete this comment to keep the history clean :)
inputPropshere gives error:Rest types may only be created from object types.ts(2700)I think this should work, i.e.
inputPropsshould essentially be of typeInputHTMLAttributes & TextareaHTMLAttributes, minus of course the fields that were plucked out.kentcdodds commentedon Nov 11, 2020
Just want to make sure that I'm not missing something. To be clear the PRs referenced, merged, and released above added support for object spread, but did not add support for type spread (as noted in the original post). So today, this is not supported:
Is that correct? Today, I'm using this handy utility I copy/pasted from stackoverflow:
Is there a better way to accomplish this today? I'm guessing that's what this issue is intended to address, but I'm happy to open a new one if I'm missing something. Thanks!
sandersn commentedon Nov 11, 2020
Yes. We never found the balance of correctness and usability for spread types that justified their complexity. At the time, React higher-order components also required negated types in order to be correctly typed. Finally, we decided that since we'd been using intersection as a workaround for so long, that it was good enough.
Just glancing at your
Spreadfrom stack overflow, it looks a bit simpler than the one I remember Anders writing to test conditional types. So it might be missing something, or it might have been improved since that time.kentcdodds commentedon Nov 11, 2020
Thanks Nathan Shively-Sanders (@sandersn)! I've also found type-fest which has a
Mergeutility which does a good job of this as well.danielo515 commentedon Mar 2, 2021
This is a very handy functionality to not having to write that many types. Flow allows this so it is doable. If the original proposition is too complex, maybe a more limited one may be implemented, but just ignoring it is not the way to go IMO
istarkov commentedon Oct 19, 2021
Kent C. Dodds (@kentcdodds) Not sure it works for all cases but shorter
TS Playground
benwiley4000 commentedon Nov 11, 2021
I'll share a very specific example of how this could be useful. With the Cypress end-to-end testing framework you register global commands with
Cypress.Commands.add('myCommand', myCommandFunction)and you use them later by callingcy.myCommand(). Thecyglobal implements theCypress.Chainableinterface so you need to declare your extra utils on this interface.So let's say you have a
commands.tswhere you declare your functions:Then in an
index.d.tsthe cleanest thing you can do is:But it would be nice if I could just add all my functions implicitly at once:
That way I can register new commands by just adding a new exported function to
commands.tswithout having to remember to update another file for typing.tilgovi commentedon Feb 24, 2022
GioAc96 commentedon Mar 21, 2023
Ivan Starkov (@istarkov)'s solution is incomplete. Take the following example:
Test['a']should evaluate toboolean, instead it evaluates toboolean | undefined. I advice people to stick to Kent C. Dodds (@kentcdodds)'s answerGioAc96 commentedon Mar 21, 2023
I think you mean
Spread,Mergedoes not correctly evaluate spreads.qwertie commentedon Jan 10, 2024
Um... I guess everyone else understands this given there are hundreds of upvotes and I'm the only 😕, but why does one need to "convert the syntax to binary syntax"? The obvious syntax is like
I was imagining that maybe binary
...represented a "compute difference" operator perhaps, but thenrest(A, P)was introduced to do a very similar thing (though in a confusing way that is syntactically unlike anything else in TypeScript or JavaScript) so I'm still confused 😕.d07RiV commentedon Jul 30, 2024
Since this has been marked as duplicate of #11100 I have to comment here, even though it's unrelated to spread operator.
What is the reason for
Object.assign(foo, bar)allowing properties offooto be overridden with different types? I think in any situation where the first argument is not a literal, it should treat the remaining arguments as partial of it's type?