microsoft / TypeScript

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

Support proposed ES Rest/Spread properties #2103

Closed fdecampredon closed 8 years ago

fdecampredon commented 9 years ago

es7 proposal : https://github.com/sebmarkbage/ecmascript-rest-spread

Spread properties

Typing

In my opinion the goal of this method is to be able to duplicate an object and changing some props, so I think it's particularly important in this case to not check duplicate property declaration :

var obj = { x: 1, y: 2};
var obj1 = {...obj, z: 3, y: 4}; // not an error

I have a very naive type check algorithm for a similar feature (JSXSpreadAttribute) in my little jsx-typescript fork: I just copy the properties of the spread object in the properties table when I encounter a spread object, and override those property if I encounter a declaration with a similar name.

Emitting

jstransform use Object.assign, babel introduce a shim:

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

We could either force the presence of assign function on ObjectConstructor interface, or provide a similar function (with a different name).

I think that the optimal solution would be to not emit any helper in es6 target (or if Object.assign is defined), and to emit an helper function for es5, es3.

var obj = { x: 1, y: 2};
var obj1 = {...obj, z: 3};

/// ES6 emit
var obj = {x: 1, y: 2};
var obj1= Object.assign({}, obj, { z: 3 });

//ES3 emit
var __assign = function (target) { 
    for (var i = 1; i < arguments.length; i++) { 
        var source = arguments[i]; 
        for (var key in source) { 
            if (Object.prototype.hasOwnProperty.call(source, key)) { 
                target[key] = source[key];
            } 
        } 
    } 
    return target; 
};

var obj = {x: 1, y: 2};
var obj1= __assign({}, obj, { z: 3 });

Rest properties

Typing

For simple object the new type is a subtype of the assignation that does not contains properties that has been captured before the rest properties :

var obj = {x:1, y: 1, z: 1};
var {z, ...obj1} = obj;
obj1// {x: number; y:number};

If the destructuring assignment has an index declaration, the result has also a similar index declaration:

var obj: { [string: string]: string };
var {[excludedId], ...obj1} = obj;
obj1// { [string: string]: string };

new/call declarations are obviously not captured:

var obj: { (): void; property: string};
var { ...obj1} = obj;
obj1// { property: string };

Emitting

It is not possible to emit rest properties without an helper function, this one is from babel:

var obj = {x:1, y: 1, z: 1};
var {z, ...obj1} = obj;
var __objectWithoutProperties = function(obj, keys) {
    var target = {};
    for (var i in obj) {
        if (keys.indexOf(i) >= 0) continue;
        if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
        target[i] = obj[i];
    }
    return target;
};

var obj = {x:1, y: 1, z: 1};
var z = obj.z;
var obj1 = __objectWithoutProperties(obj, ["z"]);

Edit: added some little typing/emitting example

SoonDead commented 8 years ago

This operator is one of the main reasons we decided to use babel in a project instead of typescript.

Completely changes the game for handling objects immutably.

niondir commented 8 years ago

As alternative is there anything planned that let you plug in custom transformation to TSC like you can do in babel? Maybe even based on the same API, that would be awesome and solve problems like this.

basarat commented 8 years ago

As alternative is there anything planned that let you plug in custom transformation to TSC like you can do in babel

@Niondir Yes. Just search for the [Transforms] tags. These are needed to get proper async/await/generator transpilation into the TypeScript compiler :rose:

niondir commented 8 years ago

After a little search, you are referencing this? https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API

Sounds great.I just do not finde anything related to the Object Spread for that API. Maybe it would be a nice little project to solve this issue based on the Compiler API :)

basarat commented 8 years ago

@Niondir sorry for not providing better help in my original message. I am talking about https://github.com/Microsoft/TypeScript/issues/5595 < which will allow pluggable emit for ESNext syntax. TypeScript is a bit more involved than Babel because it needs to have semantic type system understanding of your code so object spread work will probably need to come from TypeScript team.

Having a plugin based emitter would make this easier. Yes you would use the compiler API (or potentially fork the compiler in the beginning till a plugin system is made public + for the semantic / scanner logic)

brunolm commented 8 years ago

This might be a question for another issue, but wouldn't it be possible to add an option "allow-experimental" and have TypeScript allow future things? For example, allow spread operator and output as is. I can handle that with babel afterwards.

This is a must have feature for some projects I'm working on and I cannot migrate without essential stuff like this. It would be awesome if I could.

sandersn commented 8 years ago

I'm not sure how we'll eventually ship it, but I started work on rest/spread in object literals and destructuring. Right now the plan is to use our __assign polyfill for emit.

asvetliakov commented 8 years ago

This is the best news that i heard in past 3 months!

sandersn commented 8 years ago

Unfortunately, it turns out that getting this to work with generics is a lot of work. You have to properly support code like this:

function addId<T>(t: T): {...T, id: number} {
    return { ...t, id: 1 };
}

As you can see, addId returns a new kind of type, an object type that includes a spread operator.

After some discussion, we decided to delay until 2.1 and look at this again. We need to focus on getting 2.0 features done right now.

dallonf commented 8 years ago

Forgive my ignorance... but it feels like addId in that case be able to return T & { id: number }? Or is there some quirk of union types that stops that from being an optimal solution?

asvetliakov commented 8 years ago

I think he mean that actually T should be checked, since you can pass anything to T

addId<boolean>(true);
addId<number>(5);

Babel silently ignores such statements let a = { ...true } but i think they shouldn't be valid

DanielRosenwasser commented 8 years ago

It's not T & { id: number } because if T has an id property, it will get overridden.

On the other hand, the intersection, the resulting type of id would be the intersection of T's id type and number.

So what @sandersn is saying is that you need a separate type operator.

JabX commented 8 years ago

While we'd definitely benefit from a better typing solution here, Object.assign already exists and naively use intersections as an output type. Why couldn't we have, as a stop gap measure, the rest operator doing exactly what Object.assign does ?

It would ease a lot of concerns about the missing feature and it wouldn't bring unwanted behaviour since people already use Object.assign instead.

BLamy commented 8 years ago

Was actually extremely surprised this wasn't supported.

nschurmann commented 8 years ago

As soon as we get this the merrier. Will work great with react!

michaelmesser commented 8 years ago

I would be happy even if it was not typed

aluanhaddad commented 8 years ago

@2426021684 It might sound reasonable but consider the implications. If it is not typed, that is to say typed as any, any uses of the syntax will percolate any to the leaves of expressions.

brunolm commented 8 years ago

@aluanhaddad it would be the same as Object.assign which is the workaround everyone has to use because they have no choice (see JabX post above)

pelotom commented 8 years ago

Please consider implementing @JabX's suggestion in the near term. This proposal is stage 3 in all but name, it is definitely going to be a part of the standard, and has very clear and simple semantics. Putting in place the syntactic support for property spreads + naive typing of Object.assign would be a very helpful stopgap while we wait for the "right" implementation. Don't let perfect be the enemy of good.

Pajn commented 8 years ago

Rest properties is extremely important with React 15.2 as can be seen here https://facebook.github.io/react/warnings/unknown-prop.html

const divProps = Object.assign({}, props)
delete divProps.layout

is very ugly, especially if the amount of props on a component is higher

zpdDG4gta8XKpMCd commented 8 years ago

for those who can't wait anymore here is a workaround:

function steal(result: any, data: any): any {
    for (var key in data) {
        if (value.hasOwnProperty(key)) {
            result[key] = data[key];
        }
    }
    return result;
}

export class SameAs<a> {
    constructor(public result: a) { }
    public and<b>(value: b): SameAs<a & b> {
        return new SameAs<a & b>(steal(this.result, value));
    }
}
export function sameAs<a>(value: a): SameAs<a> {
    return new SameAs(steal({}, value));
}

// example of use:

const mixture = sameAs(one).and(anotherOne).and(yetAnotherOne).result; // <-- ta-da!
patsissons commented 8 years ago

Ignore this post, see below for a better implementation

Here is what I came up with for a poor (typescript) coder's destructuring operation:

declare interface ObjectConstructor {
  destruct<T extends Object>(data: T, props: any): T;
  destruct<T extends Object>(data: T, ...propNames: string[]): T;
}

function destruct<T extends Object>(data: T, ...removals: string[]) {
  const rest = <T>{};

  const keys = removals.length === 1 && typeof removals[0] === 'object' ?
    Object.getOwnPropertyNames(removals[0]) :
    <string[]>removals;

  Object
    .getOwnPropertyNames(data)
    .filter(x => keys.indexOf(x) < 0)
    .forEach(x => {
      (<any>rest)[x] = (<any>data)[x];
    });

  return rest;
}

Object.destruct = destruct;

// Usage example:

const srcObj = { A: 'a', B: 'b', C: 'c' };
// destruct using an object template
const onlyC = Object.destruct(srcObj, { A: null, B: null });
// destruct using property names
const onlyA = Object.destruct(srcObj, 'B', 'C');

The obvious advantage of using an object layout is that you can type the layout and at the very least get type conflicts if you happen to refactor anything.

Update (also ignore this, see below for even better)

I have reworked this (after messing around with it in a real world environment) and came up with some much easier to work with functions.

function deconstruct<TResult, TData>(
  result: TResult,
  data: TData,
  onHit: (result: TResult, data: TData, x: string) => void,
  onMiss: (result: TResult, data: TData, x: string) => void,
  propNames: string[]
  ) {

  Object
    .getOwnPropertyNames(data)
    .forEach(x => {
      if (propNames.indexOf(x) < 0) {
        if (onMiss != null) {
          onMiss(result, data, x);
        }
      }
      else {
        if (onHit != null) {
          onHit(result, data, x);
        }
      }
    });

  return result;
}

// shallow clone data and create a destructuring array of objects
// i.e., const [ myProps, rest] = destruct(obj, 'propA', 'propB');
function destruct<T>(data: T, ...propNames: string[]) {
  return deconstruct(
    [ <T>{}, <T>{} ],
    data,
    (r, d, x) => (<any>r[0])[x] = (<any>d)[x],
    (r, d, x) => (<any>r[1])[x] = (<any>d)[x],
    propNames
  );
}

// shallow clone data and create a destructuring array of properties
// i.e., const [ propA, propB, rest] = destructProps(obj, 'propA', 'propB');
function destructProps(data: any, ...propNames: string[]) {
  return deconstruct(
    [ <any>{} ],
    data,
    (r, d, x) => r.splice(r.length - 1, 0, d[x]),
    (r, d, x) => r[r.length - 1][x] = d[x],
    propNames
  );
}

// shallow clone data and remove provided props
// i.e., const excluded = omit(obj, 'excludeA', 'excludeB');
function omit<T>(data: T, ...propNames: string[]) {
  return deconstruct(
    <T>{},
    data,
    null,
    (r, d, x) => (<any>r)[x] = (<any>d)[x],
    propNames
  );
}

The typing on these functions makes things much easier to work with. I am specifically working with these in React, so having strong typing on my known props is very handy. There is an additional piece of code using these functions that is particularly handy for react:

const [ props, restProps ] = destruct(omit(this.props, 'key', 'ref'), 'id', 'text');

In this case, props is typed just like this.props, but doesn't contain any of the props meant for transferring down the line (which live now in restProps)

pelotom commented 8 years ago

IMO, not much is gained by posting non-typesafe workarounds for this problem.

patsissons commented 8 years ago

agreed, i'm working on a more typesafe version right now.

patsissons commented 8 years ago

Another stab at this:

The main object extension:

declare interface ObjectConstructor {
    rest<TData, TProps>(data: TData, propsCreator: (x: TData) => TProps, ...omits: string[]): { rest: TData, props: TProps };
}

function rest<TData, TProps>(data: TData, propsCreator: (x: TData) => TProps, ...omits: string[]) {
  const rest = <TData>{};
  const props = <TProps>propsCreator.apply(this, [ data ]);

  Object
    .getOwnPropertyNames(data)
    .filter(x => props.hasOwnProperty(x) === false && omits.indexOf(x) < 0)
    .forEach(x => {
      (<any>rest)[x] = (<any>data)[x];
    });

  return {
    rest,
    props,
  };
}

Object.rest = rest;

A React specific extension:

declare module React {
  interface Component<P, S> {
    restProps<T>(propsCreator: (x: P) => T, ...omits: string[]): { rest: P, props: T };
  }
}

function restProps<P, S, T>(propsCreator: (x: P) => T, ...omits: string[]) {
  return Object.rest((<React.Component<P, S>>this).props, propsCreator, ...omits.concat('key', 'ref'));
}

React.Component.prototype.restProps = restProps;

Some sample usage:

const src = { A: 'a', B: 'b', C: 'c' };
const { rest, props } = Object.rest(src, x => {
    const props = { A, C } = x;
  return { A, C };
});

And a sample of using the react extension:

const { rest, props } = this.restProps(x => {
  const { header, footer } = x;
  return { header, footer };
});

All typing should be preserved. the only part that bugs me is the props creator because it requires duplication. I think I could hack it and assume the destructured object lives at _a but that seems dangerous... _NOTE_: _a is not even a viable hack, as it would be equivalent x.

Here is a Fiddle to play around with.

patsissons commented 8 years ago

I'm thinking returning an array might make more sense, as you can rename the outputs as needed.

scratch that, this would prevent getting proper typing for the props.

ddaghan commented 8 years ago

Since mostly people here want to use this with react, why not implement this just for *.tsx files until it reaches stage 3?

Redux reducers can be written as *.tsx files too, just obj typecasting instances should be converted to obj as Type

olegman commented 8 years ago

This very useful not only for react, but in redux reducers for example.

kitsonk commented 8 years ago

I think the tags and missing milestone on this are a bit out of date. Because this is committed and being worked on (#10727) for TypeScript 2.1. So no need to debate the value of it anymore.

ping @mhegazy

mhegazy commented 8 years ago

Updated the tags. I should clarify that the reason this has not been implemented yet is not that the value, but rather the complexity of the type system implementation. the transformation it self is rather trivial, i.e. {...x} into Object.assign({}, x). the issue is how these types are pretested and how they behave. for instance for a generic type parameter T is {...T} assignable to T, what about {x: number, ...T}, what about if T has methods, etc.. https://github.com/Microsoft/TypeScript/issues/10727 provides a more in depth explanation of the type system changes needed.

pelotom commented 8 years ago

I fully appreciate that the type system augmentations required for an ideal treatment of this feature are not trivial, and it's great to see that they are being enthusiastically tackled! I would just reiterate that,

putting in place the syntactic support for property spreads + naive typing of Object.assign would be a very helpful stopgap while we wait for the "right" implementation. Don't let perfect be the enemy of good.

filipesilva commented 8 years ago

It seems like the proposal has reached stage 3: https://github.com/tc39/proposals

bloadvenro commented 8 years ago

@ddaghan your example with tsx won't work

Zeragamba commented 8 years ago

any timeframe for this feature?

dsifford commented 8 years ago

@SpyMaster356 I've been lurking this for a while and it looks like it's close. @sandersn has been kicking ass on this for (at least) the past few weeks. 🙌

You can follow along here (https://github.com/Microsoft/TypeScript/pull/12028) and here (https://github.com/Microsoft/TypeScript/pull/11150)

xooxdoo commented 8 years ago

Some one should update the Roadmap

aaronbeall commented 7 years ago

It seems that using this feature allows the assignment of unknown props:

interface State {
  a: string;
  b: number;
}

let state: State = { a: "a", b: 0 };

state = {...state, x: "x"}; // No error

I was expecting an error that x is not a known property of State. Is this not how the feature works?

For example, my current workaround before this PR was this:

state = update(state, { x: "x" }); // Error: Property 'x' is missing in type 'State'.

function update<S extends C, C>(state: S, changes: C): S {
  return Object.assign({}, state, changes);
}

Is it possible to achieve this with object spread/rest?

mhegazy commented 7 years ago

Object Rest and Spread, as per the ES proposal, behaves similar to Object.assign. the last mention of the property "wins". no errors are reported. in the example you had, the type of {...state, x:"X"} is { a: string, b:number, x:string }; and this type is assignable to State and thus the assignment works. this is the same as saying let state: State = { a: "a", b:0, x: "X" }; would be allowed as well.

aaronbeall commented 7 years ago

But that's what I'm confused about: let state: State = { a: "a", b:0, x: "X" } gives the error Object literal may only specify known properties, and 'x' does not exist in type 'State' which is what I want... why is it a valid assignment when coming out of an object literal containing a spread?

mhegazy commented 7 years ago

sorry.. object literals are a special case. my example was wrong. here is a better example:

let obj = { a: "a", b:0, x: "X" };
let state: State = obj; // OK

The issue here is if rather subjective. When the compiler sees let state:State = { a: "a", b:0, x: "X" }, chances are x is a typo, either a stale propoerty that was left off after refactoring, or a type for an optional property, that is why it is reported as an error.

however, if you spread an object, let's say let myConfig : State= { a: 1, ...myOtherBigConfigBag}, if the myOtherBigConfigBag has a few properties that you do not care about, you just need the a and the b, an error here would force you to keep these two interfaces in sync, or cast to make these errors go away.

that said. we should reconsider this decision. filed https://github.com/Microsoft/TypeScript/issues/12717 to track that.

aaronbeall commented 7 years ago

I see. I love your idea in #12717, that's exactly what I was thinking. I would actually like such behavior even for the spread props but I can see your point that it's very subjective and might be annoying for some common JS use cases.

bondz commented 7 years ago

@aaronbeall I agree, it would be annoying for common JS use cases... Most of the time, you just want to ensure the object has the shape of the interface specified and not directly inspect the spread object, the current implementation is okay IMO

olmobrutall commented 7 years ago

Hi guys, Congrats for the great release! Now is time for a deserved rest... speacking of whitch I've an issue with the rest operator :)

Using React you tipically have a component like:

export interface MyLinkProps extends React.HTMLAttributes {
    myUrl: string
}

class MyLink{

    render(){
      const {myUrl, ...attrs } = this.props;
     return <a href={calculate(myUrl)} ...attrs>Click here</a>;
   }
}

The issue is that when you hover with the mouse over attrs you get the list of all the properties (hundreds) instead of React.HtmlAttributes.

I know that typescript is structurally typed and all that, but could be possible to assign an explicit type to the rest variable?

Some alternatives:

    const {myUrl, ...attrs as React.HtmlAttributes } = this.props; //Is not really a casting

    const {myUrl, ...attrs : React.HtmlAttributes } = this.props; //conflicts with ES6?

    const attrs : React.HTMLAttributes; 
    const { myUrl, ...attrs } = this.props;  //re-declare the variable
aaronbeall commented 7 years ago

@bondz Well, its not true in 100% of the uses in my project. :) But in another project it may be very annoying. In my case I'm using Redux and React and making heavy use of immutable state, which means to update or create state objects I must copy all props onto a new object literal... in all cases I want 100% type safety that I'm trying to copy the right data onto the target state interface. Currently I use functions to ensure this safety, but I would prefer to use object spread because its cleaner (readable, expressive, standard syntax.) But in someone else's project they might want different behavior because they are using a lot of untyped or loose object structures, so I see how it's quite subjective. I think the #12717 suggestion is a good middle ground (and more consistent with existing object literal type safety.)

evisong commented 7 years ago

https://github.com/Microsoft/TypeScript/issues/2103#issuecomment-145688774 We want to wait for the proposal to reach Stage 3 before addressing this.

Seems it's already state 3, any plan of supporting this?

Btw, we use VSCode mainly for ES development, not TS yet :)

bondz commented 7 years ago

@evisong this feature has shipped and is already available in the latest version of vsCode. Update your vsCode to version 1.8