microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.31k stars 12.53k forks source link

Add spread/rest higher-order types operator #10727

Open sandersn opened 8 years ago

sandersn commented 8 years ago

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

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.

pelotom commented 6 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.

mshoho commented 6 years ago

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

dead-claudia commented 6 years ago

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

dead-claudia commented 6 years ago

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.

maiermic commented 6 years ago

@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
}
lucasbasquerotto commented 6 years ago

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

dead-claudia commented 6 years ago

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

mshoho commented 6 years ago

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

pelotom commented 6 years ago

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

maciejw commented 6 years ago

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
lucasbasquerotto commented 6 years ago

@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
}
pelotom commented 6 years ago

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

sebald commented 6 years ago

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.

Hotell commented 6 years ago

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']
sebald commented 6 years ago

@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'];
Hotell commented 6 years ago

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

robbyemmert commented 6 years ago

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!

punmechanic commented 6 years ago

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
}
ahejlsberg commented 6 years ago

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.

ahejlsberg commented 6 years ago

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

MohamadSoufan commented 6 years ago

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!

sandersn commented 6 years ago

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

exsoflowe commented 5 years ago

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
    }
);
MartinJohns commented 5 years ago

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

craigkovatch commented 5 years ago

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.

kentcdodds commented 4 years ago

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!

sandersn commented 4 years ago

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.

kentcdodds commented 4 years ago

Thanks @sandersn! I've also found type-fest which has a Merge utility which does a good job of this as well.

danielo515 commented 3 years ago

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 commented 3 years ago

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

TS Playground

benwiley4000 commented 3 years ago

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.

tilgovi commented 2 years ago

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 }
GioAc96 commented 1 year ago

@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

GioAc96 commented 1 year ago

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.

qwertie commented 10 months ago

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

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

d07RiV commented 4 months ago

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?