Typescript: Add spread/rest higher-order types operator

Created on 6 Sep 2016  ยท  78Comments  ยท  Source: microsoft/TypeScript

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.

  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 ... 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

  1. A has a property P or B has a property P, and
  2. Either A.P or B.P is not a method.

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.

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.

Revisit Suggestion

Most helpful comment

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!

All 78 comments

@ahejlsberg @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.

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.

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.

Interfaces are the issue. it is common for ppl to define interfaces as hierarchy to share declarations. so are these "own" or not?

I'm having trouble coming up with an example. Here's what I got so far.

interface B {
  b: number
}
interface I extends B {
  a: number
}
class C implements I { // error, C not assignable to I
  c: number
}

let ib: { ...C } = { ...new C() }
let ib2: { a: number, b: number, c: number} = ib; // error, C and { ...C } don't have a or b.

I gets b from B, but in what circumstance would b not be treated as an own property? In this example, C doesn't have a OR b, 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:

class K implements B {
  b: number
}
class C extends K implements I {
  a: number
}
let a: { ... C } = { ... new C() }
let ab: { a: number, b: number } = a; // error, { ... C } doesn't have b.

I meant speading a value whose tupe is an interface:

interface B {
  b: number
}
interface I extends B {
  a: number
}

var i: I;

var x = {...i}; // is x {a: number} or {a:number, b:number}?

@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

  1. start tracking it
  2. be over-strict
  3. be over-permissive
  4. use heuristics based on information we have and be wrong some of the time.

I'm not sure how hard (1) is.

interface I {
  a: number;
  b: number;
}
class K {
  b = 12
}
declare class C extends K implements I {
  a = 101
}
let c: I = new C();
let i: I = { ...c } // no error, but should be because 'b' was missed
i.b // error at runtime: 'b' was not an own property and got removed.

The missing piece in this example is that C instances are assignable to I but lose the _class_ inheritance information. Unfortunately this loss of information extends to enumerability as well:

interface I {
  a: number;
  f: () => void;
}
class C implements I {
  a = 12;
  f() { };
}
let c: I = new C();
let i: I = { ... c }; // no error, but should be because f is missed
i.f() // error at runtime: f was not an enumerable property

In this example, I specifies a function property but C provides a method, which is legal in TypeScript. When c gets spread, though, f drops out and i.f() will fail at runtime.

I updated the proposal to not specify own, enumerable properties. I should add a note that we deviate from the stage 2 spec, though.

Subtraction types would really be very nice to have.

For the subtraction type example, shouldn't it be error?

/** Typescript version */
function removeX<T extends { x: number, y: number }>(o: T) {
  let { x, ...rest }: T - { x: number } = o; // Error?: Type "T - { x: number }" has no property "x"
  return rest;
}

// Did you intend this?
function removeX<T extends { x: number, y: number }>(o: T): T - { x: number } {
  let { x, ...rest } = o;
  return rest;
}

Yes, thanks. I'll update the proposal.

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):

class User extends Model {
  public id?: number;
  public name?: string;
  constructor(values?: ...User);
  static update(changes: ...User): Promise<void>;
  static findAll(options?: {where?: ...User}): Promise<User[]>;
}

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 undefined because 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:

type UserWhereOptions = ...User & {
  [attribute: string]: { $and: UserWhereOptions } | { $or: UserWhereOptions } | { $gt: number };
}
class User extends Model {
  static findAll(options?: {where?: UserWhereOptions}): Promise<User[]>;
}

Regarding subtraction types, isn't a subtraction a mathematical operation on numbers? Shouldn't this really be the _difference_ operator \?

  1. 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.

    class C {
     static s: number = 12;
     i: number = 101;
    }
    let c = new C();
    let spi: { ...C } = { ... c };
    spi.i // instance properties 
    let sps: { ...typeof C } = { ...C };
    sps.s; // static properties
    
  2. It sounds like you would be interested in the partial type operator.
  3. You can add index signatures directly to your spread types:

    type UserWhereOptions = { ...User, [attribute: string]: { $and: UserWhereOptions } };
    
  4. The syntax is up in the air right now. The spread type PR implements an object-literal-like syntax but it's not final.
  1. ๐Ÿ‘
  2. Looks awesome, and work very well if it can be combined with the rest spread
  3. ๐Ÿ‘

If A and B are sets, then the relative complement of A in B,[1] also termed the set-theoretic
difference of B and A,[2] is the set of elements in B but not in A.

The relative complement of A in B is denoted B A according to the ISO 31-11 standard. It is sometimes written B - A, but this notation is ambiguous, as in some contexts it can be interpreted as the set of all elements b - a, where b is taken from B and a from A.

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 +)

(4) Good idea. Done.

I think it would be a better idea to use the syntax flowtype uses (#2710), i.e. $Diff<A, B> instead of - or \. There are other issues like #4889 where we can use the same syntax, i.e. $Partial<T> or $Shape, instead of introducing a new syntax for each case.

diff<A,B>, partial<T> or shape<T>would be ok as well.

I'm -1 on the flow syntax:

  • prefixing stuff with $ is not cool anymore (sorry jQuery :D)
  • the brackets remind me of the syntax for generics, which is confusing.
  • when you leave out the brackets, you are left with just the keyword, which matches the initial proposal for partial
  • for things like diff, A operator B is much easier to read than operator A B. Especially it makes sense when there are equivalent operators in mathematics, and other operators like union already work that way

I updated the proposal to use a binary syntax A ... B. This makes the specification simpler. TypeScript will continue to use object type literal syntax to specify spread types: { ...T, ...U } instead of T ... U.

I updated the proposal to reflect recent changes in the PR.

Specifically, assignability got stricter, spreads are no longer type inference targets, and index signatures only spread if both sides have an index signature.

We would like to pursue mapped types to model the type space for these changes.

Hi all,

Apologises if this is the wrong issue to to raise this question; I'll move as necessary if it is.

However, I've been tracking this work with keen interest and was wondering if there will be a similar type for Array or Tuple types. I know #5453 tracks something similar, although this seems to be specifically for function calls (which sadly seems blocked due to complexities arising from the various use-cases).

Essentially, the use-case I'm looking to solve is the ability to type an Array/Tuple where the first n-elements are typed, and then the remaining elements are typed as an unbounded list. To be more precise (and correct my loose language), consider:

type AT_LEAST_ONE_ELEMENT = [string, ...string]

// To support the case where a CSV row is parsed, and the first 4 columns are known, but the remaining columns (if any) will be strings
type FLEXIBLE_CSV_ARRAY = [string, number, string, boolean, ...string]

For a few bonus points, it would be great if intersections worked like:

type ONE_ELEMENT = [string]
type MANY_ELEMENTS = string[]

type AT_LEAST_ONE_ELEMENT = ONE_ELEMENT & MANY_ELEMENTS // === [string ...string]

Object spread types work with objects, and this proposal sounds more like a modification of tuples to add an array spread type for working with tuples. A lot like #6229, in fact, "Strict and open length tuples", but adding to that proposal to allow the specification of an array-spread type at the end.

I can't quite read from the proposal if I'd be able to do:

type A = {
  x: string;
  y: string;
}

type B = {
  y: string;
}

// C would only contain x
type C = rest(A, B);

Of if the last arguments to rest have to be explicit property names?

Almost; currently it's rest(A, keyof B) with the restriction that B has to be an object type. It can't be a type parameter.

The updated proposal is with the PR at #13470. I need to update this issue to match it.

There's a hack that allows to build the spread type:

interface A {
  method(): void;
  prop: number;
}

interface B {
  anotherMethod(): void;
  anotherProp: number;
}

const spread = {...({} as A & B)};
type ABSpread = typeof spread;

declare const s: ABSpread;

s.prop;  // ok
s.method;  // error
s.anotherProp;  // ok
s.anotherMethod;  // error

@sandersn Is it going to land anytime soon?

Do I understand correctly that this should allow to have normally merged object types:

```ts
type A = { x: string, y: string }
type B = { x: number, z: number}
type C = {...A, ...B} // {x: number, y: string, z: number}
````
?

It's not on our immediate list of things to ship.

You are correct, if you spread two object types, you get a new object type instead of a spread type.

Is this what's causing this code to have no issues? I would expect an error when setting bar in doSomething.

interface MyObj {
  foo?: number,
}

function doSomething(state: MyObj): MyObj {
  return { ...state, bar: 42 }
}

Nope, there are no generic types in your code. That is just a legal spread. Objects with spreads don't check for excess properties. #12997 (some design notes) has some explanation for this at the end.

Will having spread types allow for excess property checking in spreads?

That is not related.

@Kovensky #12936 tracks exact types, which is the feature you want.

Does this PR support the use case shown here? https://github.com/tc39/proposal-object-rest-spread/issues/45

Basically conditional assignment of object keys using object spread. I see that "Null/Undefined Are Ignored", but does that extend to false as well?

Currently, the emitted Javascript is correct but the compiler issues an error. We didn't think of the pattern you linked to, and thought that people would like to know if they mistakenly spread a boolean where they thought they had an object type.

We could obviously remove the error if lots of people use this pattern. I recommend that you create a new issue, label it Suggestion, and try to get upvotes for it. That's typically how we gather evidence for a change.

Syntax support aside, I think we could type the operations covered by this syntax without special types:

  • rest types: Omit (#12215)
  • spread based on Overwrite (#12215), basically MergeAll if the compiler can extract syntax { a, b, ...rest } into e.g. [{ a, b }, rest] or [{ a }, { b }, rest]

Still happening in 2.6.2...

@lednhatkhanh is that a question or a statement? I'd like to know if the spread operator is coming too..

Hi @mhegazy , @sandersn .
Is there any update or good news for this issue ?
I see only @mhegazy changes milestones in past months .
Thanks

@burabure This issue is specifically about type spreads, I believe.

What you're doing should already work with the spread operator, I think.

@Dru89 I'm such a dummy, I confused the issue XD

This is still happening as of TypeScript 2.8.3.

Hi, what is the status of it?

I would really love to have:

function merge<A, B>(a: A, b: B): A {
    return { ...a, ...b};
}

So that the compiler typechecks if the object of type B can be merged into A.

@mshoho You mean function merge<A, B>(a: A, b: B): A & B?

No. Something like:

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): A {
    return { ...a, ...b };
}

let a: AA = { prop1: 'hello', prop2: 1 };
let b: BB = { prop2: 100, prop3: true };
let c: CC = { prop2: 500, prop4: 'world' };

merge(a, b); // ok.
merge(a, c); // compiler error.

@mshoho you can approximate this with conditional types, only returning a "useful" type in the event that the second argument is compatible with the first:

function merge<A, B>(a: A, b: B): Required<A> extends Required<B> ? A & B : {} {
  return Object.assign({}, a, b) as any;
}

merge(a, b).prop2; // ok.
merge(a, c).prop2; // compiler error.

@pelotom I have something like this:

function someExternalFunction(a: AA) {
    // Something external, out of my control.
}

// And I need safely typechecked way to:

someExternalFunction(merge(a, b)); // ok.
someExternalFunction(merge(a, c)); // compiler error.

Unfortunately, {} | AA is not assignable to AA.

@mshoho did you try what I wrote? The result type is not a union...

@pelotom yes, I've just tried it.

Perhaps because I have a chain of functions with generics:

function process<A, C>(a: A, c: C) {
    someExternalFunction(merge(a, c));
}

I get this for the merge call:

[ts]
Argument of type 'Required<A> extends Required<C> ? A : {}' is not assignable to parameter of type 'AA'.
  Type '{} | A' is not assignable to type 'AA'.
    Type '{}' is not assignable to type 'AA'.
      Property 'prop1' is missing in type '{}'.

@mshoho ah, yeah, that'd do it. If you don't know what A and C are, I don't think there's a way to ensure they are compatible.

@pelotom hence my question about the status. Because as far as I understand, my problem will be automagically fixed by this ticket.

@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:

  1. If one is assignable to the other, return the intersection.
  2. Otherwise, return the union of the keys of each type not present in the
    other with for each key they both have, the key with the union of their
    values.

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: A, b: B): (
Required extends Required ? A & B :
Required extends Required
? A & B :
{[K in Exclude, keyof Required>]: A[K]} &
{[K in Exclude, 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 })

Playground Link

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 deleteing 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bgrieder picture bgrieder  ยท  3Comments

jbondc picture jbondc  ยท  3Comments

siddjain picture siddjain  ยท  3Comments

dlaberge picture dlaberge  ยท  3Comments

wmaurer picture wmaurer  ยท  3Comments