Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Destructuring (ES6) #240

Closed
RyanCavanaugh opened this issue Jul 24, 2014 · 19 comments
Closed

Destructuring (ES6) #240

RyanCavanaugh opened this issue Jul 24, 2014 · 19 comments
Assignees
Labels
ES6 Relates to the ES6 Spec

Comments

@RyanCavanaugh
Copy link
Member

Example

var [x,y] = myFun();

Codegen

var __a = myFun(); // fresh var __a
var x = __a[0];
var y = __a[1];

Type annotations

// {x: myX} is a ES6 destructuring with renaming, this looks awkward
var {x: myX: number, y: myY: number} = myPoint();
// Moving type annotation to beside destructuring left hand side
var {x: myX, y: myY}: {x: ...; y: ...} = myPoint();
@RyanCavanaugh
Copy link
Member Author

Paging @danquirk for proposal

@danquirk
Copy link
Member

Work In Progress:

Destructuring Homogeneous Arrays

For cases with homogeneous arrays destructuring is quite simple and the type system can treat the results essentially as the equivalent desugared form.

// Example
function foo { return [1, 2]; }
var [x,y] = foo();

// Codegen
var __a = foo(); // fresh var __a
var x = __a[0];
var y = __a[1];

More examples of this sort will follow at the end.

Destructuring Hetergeneous Arrays as Tuples

This is the more interesting and complicated set of cases for destructuring. How do we make this work nicely? How do we surface any proposal into a .d.ts?

function returnMultipleValues { return [1, "a"]; } // return type is {}[]
var [x,y] = returnMultipleValues();

If x and y are treated just as the desugared form then x and y will have the type {} which is not very useful. We would need arrays to carry some additional type information about their individual element types in a way that still behaves in a backwards compatible fashion.

Proposal: Arrays with Element Type Information

var xs = [1, "a"]; 
// type is {}[number, string] 
// equivalent to {}[number, string, {}. {}. ...]

Leads to:

var xs = returnMultipleValues();
var x = xs[0]; // number
var y = xs[1]; // string
var z = xs[2]; // {}, would be nice to be able to give an error

But these would not check element types and still be allowed (as it is today):

xs = ["oh", "no"]; // assign string[] to {}[]
xs = [];

Issues

var xs = returnMultipleValues(); // returns {}[number, string]
var i = 0;
var x = xs[i]; // {} unless you have data flow analysis support
var y = xs[0]; // number
x = y; // ok
y = x; // error
xs[0] = 2; // error, BREAKING CHANGE

Possible Solution: Only use array element types when processing destructuring patterns, never at other times.

Proposal: Syntax for Tuple Types

Pros:

  • Can give static errors for accessing outside the bounds of the tuple instead of behaving like an array and returning {} for those additional elements
  • Explicit, distinct from arrays

Cons

  • Doesn't play nicely with existing JavaScript

Examples

Some examples from http://fitzgeraldnick.com/weblog/50/

  • how do type annotations work in LHS of destructuring assignment? (see initial post for 2 options)
  • interface/class instances are just destructured like an object pattern?
  • do default values work with object pattern? should be basically the same as array pattern
  • is destructuring allowed in function param position? if not can we just do it anyway?
  • module import destructuring allowed?
/* Example 1 */
var xs = [1, 2];
var [x, y] = xs;

// ES5 Codegen
var __xs = xs; // fresh var
var x = __xs[0];
var y = __xs[1];



/* Example 2 */
// nested destructuring
var [foo, [[bar], baz]] = [1, [[2], 3]];

// ES5 Codegen
var __xs = [1, [[2], 3]];
var foo = __xs[0];
var bar = __xs[1][0];
var baz = __xs[1][1];



/* Example 3 */
// skipping elements
var [, , third] = ["foo", "bar", "baz"];

// ES5 Codegen
var __xs = ["foo", "bar", "baz"];
var third = __xs[2];



/* Example 4 */
// using rest pattern
var [head, ...tail] = [1, 2, 3, 4];

// ES5 Codegen
var __xs = [1, 2, 3, 4];
var head = __xs[0];
// TODO: too slow?
var tail = __xs.slice(1, __xs.length - 1);



/* Example 5 */
// destructuring objects
var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };

var { name: nameA } = robotA;
var { name: nameB } = robotB;

// ES5 Codegen
var __robotA = { name: "Bender" }; // fresh vars
var __robotB = { name: "Flexo" };

var nameA = __robotA.name;
var nameB = __robotB.name;



/* Example 6 */
// property and var names the same
var { foo, bar } = { foo: "lorem", bar: "ipsum" };

// ES5 Codegen
var __a = { foo: "lorem", bar: "ipsum" };
var foo = __a.foo;
var bar = __a.bar;



/* Example 7 */
// complicated object destructuring
var complicatedObj = {
    arrayProp: [
        "Zapp",
        { second: "Brannigan" }
    ]
};

var { arrayProp: [first, { second }] } = complicatedObj;

//ES5 Codegen
var __b = complicatedObj;
var arrayProp = __b.arrayProp;
var first = arrayProp[0];
var second = arrayProp[1].second;



/* Example 8 */
// default values
var [missing = true] = [];

// ES5 codegen
var __c = [];
var missing = __c ? __c : true;



/* Example 9 */
// destructuring in function params
function removeBreakpoint({ url, line, column }) { }

// ES5 codegen
function removeBreakpoint(__a) {
    var url = __a.url;
    var line = __a.url;
    var column = __a.url;
}



/* Example 10 */
// for loop var
for (let[key, value] of map) {
    console.log(key + " is " + value);
}

// ES5 codegen
for(let __a of map) {
    let key = __a.key;
    let value = __a.key;
}



/* Example 11 */
function returnMultipleValues() {
    return [1, 2];
}
var [foo, bar] = returnMultipleValues();

// ES5 codegen
// multiple function return values in array
var __a = returnMultipleValues(); // fresh var
var foo = __a[0];
var bar = __a[1];



/* Example 12 */
function returnMultipleValues2() {
    return { x: 1, y: 2 }
}
var [foo, bar] = returnMultipleValues2();

// ES5 codegen
// multiple function return values in array
var __a = returnMultipleValues(); // fresh var
var foo = __a.foo;
var bar = __a.bar;



/* Example 13 */
// assignment only destructuring
({ safe }) = {};

// ES5 codegen
var __a = {};
safe = __a;



/* Example 14 */
// module destructuring
const { SourceMapConsumer, SourceNode } = require("source-map");

// TODO: larger item with modules, maybe not even allowed anymore?



/* Example 15 */
interface I {
    foo: string;
    bar: number;
}

var i: I = { foo: '', bar: 2 }
var { foo, bar } = i;

// ES5 Codegen
var __a = i;
var foo = __a.foo;
var bar = __a.bar;

@Arnavion
Copy link
Contributor

Is example 8 supposed to use the ternary operator's truthiness or is it supposed to check specifically for undefined / hasOwnProperty()? Granted, I can't find anything in the harmony proposal nor MDN, and while FF Nightly does support destructuring assignment it doesn't support default values.

@RyanCavanaugh
Copy link
Member Author

See also #428

@basarat
Copy link
Contributor

basarat commented Aug 16, 2014

👍

@eggers
Copy link

eggers commented Oct 15, 2014

+1

@fdecampredon
Copy link

For syntax of type annotations I would prefer the first option :

var {x: myX: number, y: myY: number} = myPoint(); // with renaming
var {x:: number, y:: number} = myPoint(); // without renaming

than having over verbose syntax that force us to repeat the var names, especially for function arguments destructuring.

function myFunc({x::number, y::number }) {
}

Seems a lot easier to read for me than :

function myFunc({x, y}: {x: number; y: number}) {
}

@basarat
Copy link
Contributor

basarat commented Dec 14, 2014

Test required : does it work with import :

import {each, map} = require('underscore');

Asked on SO: http://stackoverflow.com/questions/27453272/destructuring-external-modules-in-typescript/27465804#27465804

@Arnavion
Copy link
Contributor

Per the ES6 spec, example 8 should be as follows:

/* Example 8 */
// default values
var [missing = true] = [];

// ES5 codegen
var __c = [];
var __c0 = __c[0];
var missing = (__c0 === undefined) ? __c0 : true;

Note that __c[0] is to be accessed only once and thus has to be lifted to a temporary, and that the default value is assigned only if the actual value is undefined.

Example 2 also has the same problem of accessing xs[1] more than once. Example 7 gets it right.

Edit: I see Ander's PR does this correctly already.


Lastly, the spec defines destructuring in terms of iterators, so:

var arr = [1, 2];
arr[Symbol.iterator] = function* () { yield "5"; }
arr[0]; // Still 1
arr[1]; // Still 2
var [foo, bar] = arr; // foo = "5", bar = undefined

The ES5 codegen that simply indexes into the array will give wrong results. TS can't really be expected to cope with this. This is only a problem for the ES5 codegen and one can say that Iterators aren't in ES5, so this is a caveat the user must be aware of if they choose to run the ES5 codegen in an environment that supports Iterators.

Even in ES6 codegen that leaves the destructuring assignment syntax unchanged, this is still a problem for the typechecker - it will infer foo and bar to be numbers but the actual iterator returns a string and undefined. To prevent this, the typechecker should validate that the Array<T>[Symbol.iterator] method only yields values of type T if overridden.

@RyanCavanaugh RyanCavanaugh added Committed The team has roadmapped this issue and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Dec 17, 2014
@RyanCavanaugh
Copy link
Member Author

@ahejlsberg - can you look at the above comment?

@ahejlsberg
Copy link
Member

@RyanCavanaugh @Arnavion I will be updating the spec with the formal typing rules for destructuring, spread, and rest as soon as I can get to it, but the current implementation in master is to the best of my knowledge correct and complete with respect to type checking and code generation.

The one area still left to do is integration with iterators. As you point out above this is relevant only when the target is ES6.

@Arnavion
Copy link
Contributor

@ahejlsberg

The integration with iterators is not relevant only when the target is ES6, it's relevant when the runtime supports Iterators. My point was that the ES5 codegen is doing the wrong thing if run on an environment that supports iterators. It's not something that TS can fix easily - it can't know that the runtime is going to support iterators and generate the correct codegen. I'm just asking that the spec / documentation for destructuring include the note that the ES5 codegen is not in line with ES6 semantics (unlike, say, classes, getters, arrow functions and the other downlevel emits which are in line).

The reason I'm talking about this situation of running ES5 codegen on partially ES6 runtimes is that this is the situation we will have today. I doubt anyone is going to compile with -target ES6. Even ignoring the fact that a lot of runtimes out there are ES5 or below (old browsers, node stable), no runtime completely supports ES6 either. Even when talking about a single feature, array literal destructuring, no browser supports the default value syntax var [missing = true] = [];. So if users do start using ES6 features in their TS codebase, they'll probably continue to use -target ES5. That's why the situation of ES5 codegen running on runtimes that partially support ES6 is one to consider.

@ahejlsberg
Copy link
Member

I'm not super concerned with scenarios where users replace an array's iterator with one that doesn't actually enumerate the elements of the array. You're right that our destructuring code generation for ES5 is an approximation of ES6 behavior, but it is a very faithful and accurate approximation for well behaved programs, and I wouldn't say it is doing the wrong thing if run in an environment that supports iterators. Rather, I'd say it doesn't give you access to the additional features offered by iterators. I think this is very much in line with the way our downlevel code generation for other ES6 features is designed (and there are certainly esoteric corners where classes and arrow functions likewise differ from native ES6).

But perhaps I'm missing your point here...

@Arnavion
Copy link
Contributor

I'm only asking for the docs / spec to mention this particular caveat. I agree that for programs that don't do funky things with Array.prototype's Iterator there will be no difference, so I can understand your point that the ES5 codegen isn't "wrong" so much as an approximation.

I just think it's a reasonable assumption for TS users to make that if an ES6 feature has a ES5 emit, then the ES5 emit has the same semantics as the ES6 emit unless documented otherwise. For example, accessors don't have an ES3 emit because it's impossible to have them there with the same semantics. If you're saying that this is not true of even arrow functions and classes, then clearly this assumption is wrong, but I'm not aware of any such differences (inheritance converting static accessors to properties is one difference but it's well-known). Modules are also different from ES6 and again that is well-documented.

@ahejlsberg
Copy link
Member

With #1878 the spec now documents our implementation of destructuring, modulo support for iterators. Keeping this bug open to track that.

@ahejlsberg ahejlsberg added Spec Issues related to the TypeScript language specification and removed Committed The team has roadmapped this issue Suggestion An idea for TypeScript labels Feb 3, 2015
@ahejlsberg
Copy link
Member

Created #1900 to track work on iterators so we can close this issue.

@ahejlsberg ahejlsberg removed the Spec Issues related to the TypeScript language specification label Feb 3, 2015
@mpawelski
Copy link

Are there any plans to add type annotation to destructured variables?
I see there were some discussion in this thread.
I would be very useful for example for React stateless functions components.
Flow team is thinking about it to
facebook/flow#235

@Arnavion
Copy link
Contributor

Arnavion commented Jul 7, 2016

@mpawelski It's a bit verbose to avoid clashing with renaming syntax but it's possible:

var a: any;

var [x, y]: [string, number] = a; // x is string, y is number

var { w, z }: { w: string, z: number } = a; // w is string, z is number

@mpawelski
Copy link

@Arnavion well, I know you can do that, but you need to write each property name twice which is far from ideal, and the purpose of descructuring is just to make code more succinct and easier to read/write, it's just a syntatic sugar. If I need to write each property name twice then it's better for me to not use destructuring at all.

I really like @fdecampredon proposition

var {x: myX: number, y: myY: number} = myPoint(); // with renaming
var {x:: number, y:: number} = myPoint(); // without renaming

and wonder if Typescript team plan on adding something similar

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
ES6 Relates to the ES6 Spec
Projects
None yet
Development

No branches or pull requests

8 participants