Open sandersn opened 8 years ago
@mshoho I don't believe it will? Unless I'm misunderstanding something, what you really need is some kind of constraint to be able to place on A
/ B
when declaring them, similar to this:
function merge<A extends B, B>(a: A, b: B): A & B {
// ...
}
If your signature is just merge<A, B>(...)
you've already lost, because A
and B
can be anything the caller likes.
@pelotom it works as I need with the spread operator without generics:
let aa: AA = { ...a, ...b }; // ok.
let bb: AA = { ...a, ...c }; // compiler error (Object literal may only specify known properties, and 'prop4' does not exist in type 'AA')
If spread will properly unroll the generics, it'll be what I need.
@pelotom Shouldn't the return type specify the alternate? Specifically, I think the return type should be this:
function merge<A, B>(a: A, b: B): (
Required<A> extends Required<B> ? A & B :
Required<B> extends Required<A> ? A & B :
{[K in Exclude<keyof Required<A>, keyof Required<B>>]: A[K]} &
{[K in Exclude<keyof Required<B>, keyof Required<A>>]: B[K]} &
{[K in keyof Required<A> & keyof Required<B>]: A[K] | B[K]}
)
Of course, this bug's proposal is to make the above insanity a type-level operator. 🙂
@mshoho Yeah, you should prefer the spread operator without it. The compiler should do the right thing, and if it doesn't, it's a bug - TypeScript should properly type it, even though there's no explicit corresponding type syntax for it.
Oh, and also, @mshoho, there shouldn't be a need to wrap it. TypeScript provides suitable fallback behavior for ES5/ES6, so that shouldn't be a concern.
@mshoho You will still get a compile error if you use the spread operator with generics like in process
function someExternalFunction(a: AA) {
// Something external, out of my control.
}
function process<A, C>(a: A, c: C) {
someExternalFunction({ ...a, ...c }); // compile error: { ...A, ...C } is not assignable to AA
}
@mshoho It seems the answer from @isiahmeadows solves your case (great answer, although it's not very intuitive):
interface AA {
prop1: string;
prop2?: number;
prop3?: boolean;
}
interface BB {
prop2?: number;
prop3?: boolean;
}
interface CC {
prop2?: number;
prop4?: string;
}
function merge<A, B>(a: A, b: B): (
Required<A> extends Required<B> ? A & B :
Required<B> extends Required<A> ? A & B :
{[K in Exclude<keyof Required<A>, keyof Required<B>>]: A[K]} &
{[K in Exclude<keyof Required<B>, keyof Required<A>>]: B[K]} &
{[K in keyof Required<A> & keyof Required<B>]: A[K] | B[K]}
) {
return Object.assign({}, a, b) as any;
}
let a: AA = { prop1: 'hello', prop2: 1 };
let b: BB = { prop2: 100, prop3: true };
let c: CC = { prop2: 500, prop4: 'world' };
function someExternalFunction(a: AA) {
// Something external, out of my control.
console.log(a);
}
// And I need safely typechecked way to:
merge(a, b); // ok.
merge(a, c); // ok
someExternalFunction(merge(a, b)); // ok.
someExternalFunction(merge(a, c)); // ok
someExternalFunction(merge(b, c)); // compile error (it's ok)
The only drawback (other than the complex type) is that you have to do something like Object.assign({}, a, b) as any
instead of { ...a, ...b }
(I think this is what this issue is about, allowing the spread operator in generic types and returning that complex type out-of-the-box ).
The good thing in this case is that the merge
function will be defined only once and can be used anywhere, without the need for you to do nasty stuff and bypass/cast types.
The type of merge(a, c)
is:
{
prop1: string;
prop3: boolean;
} & {
prop4: string;
} & {
prop2: number;
}
so it can be assigned to AA
.
The intuition for my type is this:
If you think of this as a Venn diagram of sorts, the two types each represent circles. The type union can only view the set intersection (those in both circles), but the type intersection can view the properties of both circles. If one circle is wholly contained in the other, I can just return the bigger circle. If they simply overlap, I have to pluck out each part of that Venn diagram, since the keys in both A and B, in that intersection, have to be the value from either A or B if the key in B is optional, but it has to be the value from B if it's mandatory on B.
My type is almost correct - I just need to pluck out non-optional keys of B from the intersection and set them to B, but that's it.
On Wed, Jul 11, 2018, 13:59 Lucas Basquerotto notifications@github.com wrote:
@mshoho https://github.com/mshoho It seems the answer from @isiahmeadows https://github.com/isiahmeadows solves your case (great answer, although it's not very intuitive):
interface AA { prop1: string; prop2?: number; prop3?: boolean; } interface BB { prop2?: number; prop3?: boolean; } interface CC { prop2?: number; prop4?: string; } function merge<A, B>(a: A, b: B): ( Required extends Required ? A & B : Required extends Required ? A & B : {[K in Exclude<keyof Required, keyof Required>]: A[K]} & {[K in Exclude<keyof Required, keyof Required>]: B[K]} & {[K in keyof Required & keyof Required]: A[K] | B[K]} ) { return Object.assign({}, a, b) as any; } let a: AA = { prop1: 'hello', prop2: 1 };let b: BB = { prop2: 100, prop3: true };let c: CC = { prop2: 500, prop4: 'world' }; function someExternalFunction(a: AA) { // Something external, out of my control. console.log(a); } // And I need safely typechecked way to:merge(a, b); // ok.merge(a, c); // oksomeExternalFunction(merge(a, b)); // ok.someExternalFunction(merge(a, c)); // oksomeExternalFunction(merge(b, c)); // compile error (it's ok)
The only drawback (other than the complex type) is that you have to do something like Object.assign({}, a, b) as any instead of { ...a, ...b } (I think this is what this issue is about, allowing the spread operator in generic types and returning that complex type out-of-the-box ).
The good thing in this case is that the merge function will be defined only once and can be used anywhere, without the need for you to do nasty stuff and bypass/cast types.
The type of merge(a, c) is:
{ prop1: string; prop3: boolean; } & { prop4: string; } & { prop2: number; }
so it can be assigned to AA.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/10727#issuecomment-404258073, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBHS1aX3KYHkh5CIq-9vSeVXVxWsCks5uFj0OgaJpZM4J2HPx .
@lucasbasquerotto it's not solved for my actual use case with the chained generics:
function process<A, B>(a: A, b: B) {
someExternalFunction(merge(a, b)); // This one still complains:
// Type 'A & B' is not assignable to type 'AA'.
// Property 'prop1' is missing in type '{}'.
}
Plus someExternalFunction(merge(a, c)); // ok
should not be ok, as c has prop4 which is not present in AA.
@mshoho
function process<A, B>(a: A, b: B) { someExternalFunction(merge(a, b)); // This one still complains: // Type 'A & B' is not assignable to type 'AA'. // Property 'prop1' is missing in type '{}'. }
This will never "just work" if it has this signature... as I said before, there needs to be a constraint on A
or B
that ensures their compatibility.
this constraint on A or B is needed because
let a = {p: 0}
let b = {p: 0, q: 0}
a = b // assignable
b = a // not assignable
@mshoho
Plus someExternalFunction(merge(a, c)); // ok should not be ok, as c has prop4 which is not present in AA.
I don't see the problem here. You can pass a subclass (more specific) to a function that expects the superclass (less specific). The same is valid with interfaces. In typescript you can assign a more specific interface to a less specific one.
If you consider the interface I1 { a: A; b: B; c: C }
and the interface I2 { a: A; b: B; c: C; d: D }
, then I2 can be assigned to a variable/parameter that expects I1.
interface I1 { a: number; b: number; c: number }
interface I2 { a: number; b: number; c: number; d: number }
let a: I2 = { a: null, b: null, c: null, d: null };
let b: I1 = a; // this is ok
let c: Object = a; // this is also ok
let d: I2 = c; // error (trying to assign the less specific to the more specific)
function e(i1: I1) { }
function f(i2: I2) { }
e(a); // this is ok (I2 is more specific than I1, so it can be assigned)
e(b); // this is ok
f(a); // this is ok
f(b); // error (trying to assign the less specific to the more specific)
About your function process
, A
and B
are generics and you have no information about them, while someExternalFunction
expects an object of type AA
, so, like @pelotom said:
there needs to be a constraint on A or B that ensures their compatibility.
You can try the following, if it is acceptable in your use case:
function merge<A, B>(a: A, b: B): A & B {
return Object.assign({}, a, b) as any;
}
function process<A extends AA, B>(a: A, b: B) {
someExternalFunction(merge(a, b)); // it works
}
@lucasbasquerotto
function merge<A, B>(a: A, b: B): A & B { return Object.assign({}, a, b) as any; }
this signature is incorrect for the case where B
has overlapping inconsistent keys with A
. E.g.,
// x: number & string
const { x } = merge({ x: 3 }, { x: 'oh no' })
This proposal as I understand it would allow you to give the correct type as
function merge<A, B>(a: A, b: B): { ...A, ...B }
which says that B
overwrites A
where they overlap. I believe you can simulate this already with
type Omit<A, K extends keyof any> = Pick<A, Exclude<keyof A, K>>;
type Overwrite<A, B> = Omit<A, keyof A & keyof B> & B;
function merge<A, B>(a: A, b: B): Overwrite<A, B> {
return Object.assign({}, a, b);
}
// x: string
const { x } = merge({ x: 3 }, { x: 'hooray' });
Are there cases where this is incorrect? I'm not sure 🤷♂️ But it's not clear how to make it work in the given use case:
function process<A extends AA, B /* extends ?? */>(a: A, b: B) {
// doesn't work, compiler can't prove `merge(a, b)` is assignable to `AA`
someExternalFunction(merge(a, b));
}
An alternative typing is to require that B
must be consistent with A
where they overlap:
function merge<A, B extends Partial<A>>(a: A, b: B): A & B {
return Object.assign({}, a, b);
}
function process<A extends AA, B extends Partial<A>>(a: A, b: B) {
someExternalFunction(merge(a, b)); // it works
}
This seems mostly good, but it breaks down on this corner case:
// x: number & undefined
const { x } = merge({ x: 42 }, { x: undefined });
Not sure what can be done about that...
Has anyone got Generics + Spread-Syntax to work properly? :-/ I ran into this multiple times:
interface Response<T> {
data: T;
}
// Works
export const getData1 = (res: Response<object>) =>
({ ...res.data })
// Error
// Spread types may only be created from object types.
// (property) Response<T>.data: T extends object export
const getData2 = <T extends object>(res: Response<T>) =>
({ ...res.data })
Current workaround -> use Object.assign
and assert type.
few assertions is all you need ;)
interface Response<T extends object = object> {
data: T;
}
export const getData1 = (res: Response<object>) =>
({ ...res.data })
export const getData2 = <T extends object = object>({ data }: Response<T>) =>
({ ...data as object }) as Response<T>['data']
@Hotell Thanks for your reply 🙂 I think I didn't make this clear with my posting above.
My issue is not that I can work around generics + spread, but rather that it feels like this is a bug in TS. There are several issues mentioning this problem and https://github.com/Microsoft/TypeScript/pull/13288 was closed a few days ago 😕 So there seems to be no solution in sight.
Which is fine BTW, I am so thankful for all the work the TS team has put into this language!!! But I also want to raise awareness for this issue.
tl;dr I am currently doing this, which I find a little bit more readable than your proposal (because of less type assertions):
export const getData3 = <T extends object>(res: Response<T>) =>
Object.assign({}, res.data) as Response<T>['data'];
I know @sebald ! and I agree , though I don't think it's bug rather than not implemented feature. but TS team will add this for sure sooner than later. I'm dealing with this as well... cheers
This has been open for over 2 years now. Any progress?
This feels like a bug, since destructuring is common practice now (though it probably wasn't as much when this issue was created). { ...props }
should behave the same way as Object.assign({}, props)
(which works beautifully, by the way).
If the roadblock is simply time, resources, or technical reasons, do say something. You've got a line of people who want this fixed and may be able to offer help/suggestions. Thanks!
This is particularly a problem if you want to do something like this:
interface OwnProps<P> {
a: number
b: number
component: React.ComponentType<P>
}
type Props<P> = OwnProps<P> & P
function HigherOrderComponent({ a, b, component: Component, ...rest }: Props) {
// do something with a and b
return <Component {...rest} />
}
In this scenario, because of the absence of generic spread, there's no easy way to ensure appropriate typing. You could use Object.assign()
:
function HigherOrderComponent({ a, b, component: Component }: Props) {
const newProps = Object.assign({}, props, {
a: undefined,
b: undefined,
component: undefined
})
return <Component {...newProps} />
}
However, newProps
will have the wrong typing in this case:
type NewProps = P & {
a: undefined,
b: undefined,
component: undefined
}
Instead of
type NewProps = P & {
a: never,
b: never,
component: never
}
And unfortunately never
is not assignable to undefined
, which means we require explicit type casting for this to function.
Now, one could use the Exclude
type to fix this with a utility method if you're willing to get really hacky!
function omit<T extends object, K extends keyof T>(obj: T, ...keys: K[]) {
const shallowClone = Object.assign({}, obj)
keys.forEach(key => {
delete shallowClone[key]
})
return shallowClone
}
However, as far as I'm aware, Exclude<T, Pick<T, K>>
isn't going to work here because K
is an array of keys and there's no way to spread array keys in a type either. Another issue is that TypeScript doesn't recognise that delete
ing a field on an object should change the type of that object after the invocation, likely because that happens at runtime and it's difficult to determine exactly when that 'delete' takes place. Consider the following as a way to demonstrate why this won't work:
const a = {
a: 1,
b: 2
}
const b = {...a}
setTimeout(() => {
delete b.a // Unless TypeScript has knowledge of setTimeout there's no way to safely guarantee that b.a is `never`.
// That is to say that this code can never work because it is not knowable at compile time when 'delete' will be executed.
}, 1000)
Are there any solutions I'm missing?
The ideal solution IMO would be object spread on generics as is self-evident, but this also made me realise that being able to use spread on array type parameters would be really useful too!
function omit<T extends object, K extends keyof T>(obj: T, ...keys: K[]): Exclude<T, Pick<T, ...K>> {
const shallowClone = Object.assign({}, obj)
keys.forEach(key => {
delete shallowClone[key]
})
return shallowClone
}
We now have generic spread expressions in object literals implemented in #28234. The implementation uses intersection types instead of introducing a new type constructor. As explained in the PR, we feel this strikes the best balance of accuracy and complexity.
...and generic rest variables and parameters are now implemented in #28312. Along with #28234 this completes our implementation of higher-order object spread and rest.
Any updates on this issue after Typescript 3.2.1 came out? https://blogs.msdn.microsoft.com/typescript/2018/11/29/announcing-typescript-3-2/ it seems that spread operator now works on Generics!
@MohamadSoufan No, @ahejlsberg's comment is the current state of things. Spread expressions now have a type, but the type is an intersection, not a real spread type. This proposal is for a spread type.
Is it related issues? Is it normal behavior? Typescript Playground
// Also I tried to use https://stackoverflow.com/questions/44525777/typescript-type-not-working-with-spread-operator
interface Box {
title: string;
height: number;
width: number;
}
const obj: any = { // I hardcoded 'any' here. For example, because of external library and I can't declare
height: 54,
width: 343,
};
const resultSpread: Box = {
...obj,
title: 'Title1',
anyField: 47, // will throw ONLY if I removed hardcoded 'any' for 'obj' constant.
};
const resultAssign: Box = Object.assign(
obj,
{
title: 'Title1',
anyField: 47, // there is no errors always
}
);
@exsoflowe This is expected behavior.
In the first case of resultSpread
it does not throw an error because of the any
type. When you mark an object as any
then you lose all compiler type support. Spreading an any
object results in the object becoming any
itself. So you're creating an any
object literal, then assign that to the Box
variable. Assigning any
is possible to any other type.
In the second case of resultAssign
it is the same issue when you keep the type as any
. When you have an actual type, e.g. { width: number; height: number }
, then the type of of the Object.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 a Box
variable and the compatibility check happens. Your new type consisting of the four properties matches the interface (it has both properties of Box
) 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.assign
example it won't work because Object.assign
is 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 not Box
.
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.
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 :)
export interface TextFieldProps {
kind?: 'outline' | 'line' | 'search';
label?: React.ReactNode;
message?: React.ReactNode;
onClear?: () => void;
valid?: boolean | 'pending';
}
export function TextFieldFactory<T extends 'input' | 'textarea'>(type: T) {
return React.forwardRef<T extends 'input' ? HTMLInputElement : HTMLTextAreaElement, TextFieldProps & (T extends 'input' ? React.InputHTMLAttributes<HTMLElement> : React.TextareaHTMLAttributes<HTMLElement>)>((props, ref) => {
const { className, disabled, id, kind, label, message, onClear, valid, ...inputProps } = props;
inputProps
here gives error:
Rest types may only be created from object types.ts(2700)
I think this should work, i.e. inputProps
should essentially be of type InputHTMLAttributes & TextareaHTMLAttributes
, minus of course the fields that were plucked out.
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:
let ab: { ...A, ...B } = { ...a, ...b };
Is that correct? Today, I'm using this handy utility I copy/pasted from stackoverflow:
// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
{ [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];
// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
{ [P in K]: L[P] | Exclude<R[P], undefined> };
type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*
// Type of { ...L, ...R }
type Spread<L, R> = Id<
// Properties in L that don't exist in R
& Pick<L, Exclude<keyof L, keyof R>>
// Properties in R with types that exclude undefined
& Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;
type A = {bool: boolean, str: boolean}
type B = {bool: string, str: string}
type C = Spread<A, B>
const x: C = {bool: 'bool', str: 'true'}
console.log(x)
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!
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 Spread
from 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.
Thanks @sandersn! I've also found type-fest which has a Merge
utility which does a good job of this as well.
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
@kentcdodds Not sure it works for all cases but shorter
type IntersectPropToNever<A, B> = {
[a in keyof (A & B)]: (A & B)[a]
}
type Spread<A, B> = IntersectPropToNever<A, B> & A | B;
type A = {a: string, b: string};
type B = {b: number};
const tt: Spread<A,B> = {a: '', b: 1};
const ee: Spread<B,A> = {a: '', b: '1'};
// error Type 'string' is not assignable to type 'number'.
const tt_err: Spread<A,B> = {a: '', b: '1'};
// error Type 'number' is not assignable to type 'string'.
const ee_err: Spread<B,A> = {a: '', b: 1};
// Property 'b' is missing in type '{ a: string; }'
const ww_err: Spread<A,B> = {a: ''}
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 calling cy.myCommand()
. The cy
global implements the Cypress.Chainable
interface so you need to declare your extra utils on this interface.
So let's say you have a commands.ts
where you declare your functions:
export function myCommand1(selector: string) {
// ...
}
export function myCommand2(index: number) {
// ...
}
Then in an index.d.ts
the cleanest thing you can do is:
declare namespace Cypress {
import * as commands from './commands';
interface Chainable {
myCommand1: typeof commands.myCommand1;
myCommand2: typeof commands.myCommand2;
}
}
But it would be nice if I could just add all my functions implicitly at once:
declare namespace Cypress {
import * as commands from './commands';
interface Chainable {
...(typeof commands)
}
}
That way I can register new commands by just adding a new exported function to commands.ts
without having to remember to update another file for typing.
EDIT: The StackOverflow post linked above seems to have the tuple version now.
In case it's useful, we can combine the tuple types and recursive types to get this:
type Spread<T> =
T extends [infer Acc]
? { [Key in keyof Acc]: Acc[Key] }
: T extends [infer Acc, infer Next, ...infer Rest]
? Spread<[Pick<Acc, Exclude<keyof Acc, keyof Next>> & Next, ...Rest]>
: never;
type A = { a: boolean, x: boolean };
type B = { b: number, y: number };
type C = { a: string, b: string };
type X = Spread<[A, B]>; // { a: boolean, x: boolean, b: number, y: number }
type Y = Spread<[A, B, C]>; // { a: string, x: boolean, b: string, y: number }
@istarkov's solution is incomplete. Take the following example:
type IntersectPropToNever<A, B> = {
[a in keyof (A & B)]: (A & B)[a]
}
type Spread<A, B> = IntersectPropToNever<A, B> & A | B;
type Test = Spread<{
a: true
}, {
a?: false
}>
Test['a']
should evaluate to boolean
, instead it evaluates to boolean | undefined
. I advice people to stick to @kentcdodds's answer
Thanks @sandersn! I've also found type-fest which has a
Merge
utility which does a good job of this as well.
I think you mean Spread
,Merge
does not correctly evaluate spreads.
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
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
type A = { a:number, b?: number };
type B = { b:string };
let ab: { ...A, ...B } = { ...{ a: 1, b: 2 }, ...{ b: 'hi' } };
I was imagining that maybe binary ...
represented a "compute difference" operator perhaps, but then rest(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 😕.
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 of foo
to 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?
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 ... A
is equivalent toA ... A
andA ... A
is equivalent to{} ... A
.A ... B
is not equivalent toB ... A
. Properties ofB
overwrite properties ofA
with the same name inA ... B
.(A ... B) ... C
is equivalent toA ... (B ... C)
....
is right-associative.|
, 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 propertyP
ifA
has a propertyP
orB
has a propertyP
, andA.P
orB.P
is not a method.In this case
(A ... B).P
has the typeB.P
ifB.P
is not optional.A.P | B.P
ifB.P
is optional andA
has a propertyP
.A.P
otherwise.private
,protected
andreadonly
behave the same way as optionality except that ifA.P
orB.P
isprivate
,protected
orreadonly
, then(A ...B).P
isprivate
,protected
orreadonly
, respectively.Index signatures
The type
A ... B
has an index signature ifA
has an index signature andB
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)
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 toA
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 typeT
after the propertiesa
,b
andc
have 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 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)
removesP
fromA
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.