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

Object spread #11150

Merged
merged 103 commits into from
Nov 10, 2016
Merged

Object spread #11150

merged 103 commits into from
Nov 10, 2016

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Sep 26, 2016

Leaves #10727 still open
Fixes #2103
Leaves #11100 still open

Important Update

The PR no longer contains a spread type, only spread syntax, which creates object types. I expect a spread type or something similar to be available in future versions of TypeScript.

Updates to proposed semantics: type variable assignability

A spread type containing spread elements whose types are type variables is assignable to another spread type also containing type variables if the non-type-variable properties and index signatures are assignable as above and the the type variables are the same variables in the same order.

For example: { a: number, ...T, b: number, ...U, ...x } is assignable to { b: number, a: number, ...x ...T, ...U} and vice versa. But the similar { a: number, ...T, b: number, ...U, ...x } is not assignable to { b: number, a: number, ...x ...U, ...T} because the order of T and U is reversed.

This rule is not strictly safe, since a type parameter could override any properties that precede it (see the rule for private below), but it would be odd if { ...T, ...U } were not assignable to { ...T, ...U }. Instead, this rule assumes that the contents of a type variable will not conflict with other properties.

Important Implementation Details

Spread types use a binary representation to make recursive transformations easier. This matches the proposal but the syntax remains close to spread's expression syntax.

getSpreadType creates a spread only if the spread type contains a type parameter. Otherwise it will create an anonymous type, or union or intersection of anonymous types. The binary structure is left deep, so the left side of a spread type is either another spread (the recursive case) or an object type (the terminal case). The right side of spread type is either a type parameter or an object type. Adjacent object types are spread into a single object type, so the structure will never have two adjacent object types, only adjacent type parameters.

Updated proposal

The spread part of #10727's updated proposal follows for convenience:

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 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 { ...spread } 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 is equivalent to the properties of A. 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.
  • Associativity: (A ... B) ... C is equivalent to A ... (B ... C). ... is right-associative.
  • Distributivity: Spread is distribute over both | and &, so A ... (B | C) is equivalent to A ... B | A ... C and 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 variables

A spread type containing type parameters is assignable to another spread type if the the non-type-variable properties and index signatures are assignable as above and the type variables are the same variables in the same order.

For example: { a: number, b: number } ... T is assignable to { a: number } ...T and to T ... { a: number }. But the similar T ... U is not assignable to U ... T because the order of T and U is reversed.

Type inference

Type inference is allowed between an object type source and a spread type target with a type parameter as either the left or right side. If the type parameter is on the right, then its inferred type is the whole source type. If the type parameter is on the left, then its inferred type is the source type minus properties on the right side of the spread.

For example, if the target is T ... { a: number } and the source is { a: number, b: string }, the inferred type for T is { b: string }. Because spread types remove call and construct signatures, if the source were { a: number, b: string, (): void }, the type inferred for T would still be { b: string }.

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 A has a property P or B has a property P. 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.

Index signatures spread the same way as properties.

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.

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

@sandersn
Copy link
Member Author

@ahejlsberg can you take a look?

1. SpreadElementExpression (existing, for arrays) -> SpreadExpression
2. SpreadElement (new for object literals) -> SpreadElementExpression
What's YOUR favourite thing about Mars, Beartato?
Previously, they worked when they came from a spread type but not when
written in the object literal itself.
As elsewhere in the compiler code
@DanielRosenwasser
Copy link
Member

Identity

Is { ...number } equivalent to number? I don't think that it should be.

@sandersn
Copy link
Member Author

What are the properties of number? I don't think there are any, so { ... number } is equivalent to {}, which is the properties of number.

This is based on experimentation with V8's implementation of Object.assign, where Object.assign({}, 12) returns {}.

@DanielRosenwasser
Copy link
Member

What are the properties of number? I don't think there are any,

Well....

Usually when we think about the properties of number, we think of its apparent type Number. In the spec, before getting the property keys, the ToObject function is used on whatever value you're spreading in. So there's precedent for the same analogue here: to get the properties of number, we get the properties of Number.

However, you're right that the "correct" behavior will give you no properties because none of the properties of Number are "owned" by any instance. So it would be "wrong" to give that to users, though I can't imagine who would ever do that anyway.

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some intermediate feedback.

@@ -4273,6 +4319,21 @@ namespace ts {
setObjectTypeMembers(type, emptySymbols, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo);
}

function resolveSpreadTypeMembers(type: SpreadType) {
// The members and properties collections are empty for spread types. To get all properties of an
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an -> a

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this should be JSDoc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code no longer exists.

// spread type use getPropertiesOfType.
let stringIndexInfo: IndexInfo = getIndexInfoOfSymbol(type.symbol, IndexKind.String);
let numberIndexInfo: IndexInfo = getIndexInfoOfSymbol(type.symbol, IndexKind.Number);
for (let i = type.types.length - 1; i > -1; i--) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>= 0?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code no longer exists.

export interface IntersectionType extends UnionOrIntersectionType { }
/* @internal */
export interface SpreadElementType extends ResolvedType {
isDeclaredProperty?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add documentation about what this means?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type no longer exists. getSpreadType acts on this information immediately instead of preserving it.

@@ -5589,15 +5729,67 @@ namespace ts {
function getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node: Node, aliasSymbol?: Symbol, aliasTypeArguments?: Type[]): Type {
const links = getNodeLinks(node);
if (!links.resolvedType) {
// Deferred resolution of members is handled by resolveObjectTypeMembers
const type = createObjectType(TypeFlags.Anonymous, node.symbol);
const isSpread = (node.kind === SyntaxKind.TypeLiteral &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasSpread

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an isTypeLiteral s that you don't have to subsequently cast?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but it only saves a couple of casts anyway.

const isSpread = (node.kind === SyntaxKind.TypeLiteral &&
find((node as TypeLiteralNode).members, elt => elt.kind === SyntaxKind.SpreadTypeElement));
let type: ObjectType;
if (isSpread) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider refactoring the entire body here into its own function to keep it clean.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Done.

const type = createObjectType(TypeFlags.Anonymous, node.symbol);
const isSpread = (node.kind === SyntaxKind.TypeLiteral &&
find((node as TypeLiteralNode).members, elt => elt.kind === SyntaxKind.SpreadTypeElement));
let type: ObjectType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You never use type in the first branch, so do you really need to declare it up here?

@@ -5851,6 +5879,67 @@ namespace ts {
return links.resolvedType;
}

/**
* Since the source of spread types are object literals and type literals, which are not binary,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not type literals anymore

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
else if (!(rightProp.flags & SymbolFlags.Method && isFromSpread) &&
!(rightProp.flags & SymbolFlags.SetAccessor && !(rightProp.flags & SymbolFlags.GetAccessor))) {
// skip methods from spreads and accessors with setters but no getters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why accessors with setters & no getters? Is this per spec?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline, "hilariously yes"
.

let stringIndexInfo: IndexInfo;
let numberIndexInfo: IndexInfo;
if (left === emptyObjectType) {
// for the first spread element, left === emptyObjectType, so take the right's string indexer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the right element is an empty object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's treated like any other object type. emptyObjectType is only a marker of the start of the spread type because the checkObjectLiteral uses getSpreadType in a left fold manner, and emptyObjectType is initial value of the fold.

|| leftProp.name in skippedPrivateMembers) {
continue;
}
if (leftProp.name in members) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like members should be renamed to rightProps

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's the members of the resulting spread. If right doesn't have a property that left does, then members will have leftProp. So calling it rightProps is not accurate.

result.isReadonly = isReadonly;
result.type = containingType.flags & TypeFlags.Union ? getUnionType(propTypes) : getIntersectionType(propTypes);
result.type = containingType.flags & TypeFlags.Intersection ? getIntersectionType(propTypes) : getUnionType(propTypes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert If you feel like it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -3564,6 +3571,14 @@ namespace ts {
return unknownType;
}

function getTypeOfSpreadProperty(symbol: Symbol) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a need for a new kind of symbol. We should just create regular SymbolFlags.Property symbols and compute the type eagerly.

if (type.flags & TypeFlags.UnionOrIntersection) {
return getPropertiesOfUnionOrIntersectionType(<UnionType>type);
}
return getPropertiesOfObjectType(type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? I much prefer the existing terser functional style.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a separate spread type, I had more code in here that made the verbose style more readable. Restored.

result.containingType = containingType;
result.hasNonUniformType = hasNonUniformType;
result.isPartial = isPartial;
result.declarations = declarations;
if (declarations.length) {
result.valueDeclaration = declarations[0];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added? I don't think it is correct to say the first of an arbitrarily ordered list is the definitely value declaration. Plus, the declaration doesn't actually correctly reflect the type of the union property.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking index constraints in checkIndexConstraints relies on valueDeclaration. It's not needed now that we don't create synthetic symbols for spread any more, so I removed it. But in general checkIndexConstraints works on synthetic properties by mistake. If choosing an arbitrary valueDeclaration isn't right, the checkIndexConstraints needs to stop relying on it.

@@ -5041,7 +5069,7 @@ namespace ts {
const declaration = getIndexDeclarationOfSymbol(symbol, kind);
if (declaration) {
return createIndexInfo(declaration.type ? getTypeFromTypeNode(declaration.type) : anyType,
(getModifierFlags(declaration) & ModifierFlags.Readonly) !== 0, declaration);
(getModifierFlags(declaration) & ModifierFlags.Readonly) !== 0, declaration);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually don't indent like this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

* and right = the new element to be spread.
*/
function getSpreadType(left: Type, right: Type, symbol: Symbol): ResolvedType | IntrinsicType {
Debug.assert(!!(left.flags & (TypeFlags.Object | TypeFlags.Any)) && !!(right.flags & (TypeFlags.Object | TypeFlags.Any)), "Only object types may be spread.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider permitting intersection types as well? I understand how union types are harder and would require "lifting" to produce a resulting union type, but it seems like intersections would just work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did, and it would be simple to restore the code I had there earlier. The reason I decided against is that it gets harder to issue a correct, understandable error in checkObjectLiteral because intersections that contain type parameters are still not allowed. And I assume that we'll have a good story for all kinds of type operators shortly after 2.1 ships, so a restriction on intersections won't have lasting problems.

result.declarations = declarations;
if (declarations.length) {
result.valueDeclaration = declarations[0];
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, any reason you're setting the valueDeclaration property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Removed.

result.valueDeclaration = declarations[0];
}
result.isReadonly = isReadonlySymbol(leftProp) || isReadonlySymbol(rightProp);
members[leftProp.name] = result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just compute the type here and set it directly on the new symbol.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

checkIndexConstraints(type);
checkTypeForDuplicateIndexSignatures(node);
checkObjectTypeForDuplicateDeclarations(node);
if (type.flags & TypeFlags.Object) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this not be an object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from when { ...T } syntax would produce a spread type. Removed.

@@ -17140,7 +17270,7 @@ namespace ts {
// perform property check if property or indexer is declared in 'type'
// this allows to rule out cases when both property and indexer are inherited from the base class
let errorNode: Node;
if (prop.valueDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
if (prop.valueDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restore indentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

const name = prop.name;
if (name.kind === SyntaxKind.ComputedPropertyName) {
if (name && name.kind === SyntaxKind.ComputedPropertyName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can name now be undefined where previously it couldn't?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore. Removed.

Instead of getSpreadType.

Also clean up special-case handling inside getSpreadType to be more
readable.
Copy link
Member Author

@sandersn sandersn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed PR comments

if (type.flags & TypeFlags.UnionOrIntersection) {
return getPropertiesOfUnionOrIntersectionType(<UnionType>type);
}
return getPropertiesOfObjectType(type);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a separate spread type, I had more code in here that made the verbose style more readable. Restored.

@@ -5041,7 +5069,7 @@ namespace ts {
const declaration = getIndexDeclarationOfSymbol(symbol, kind);
if (declaration) {
return createIndexInfo(declaration.type ? getTypeFromTypeNode(declaration.type) : anyType,
(getModifierFlags(declaration) & ModifierFlags.Readonly) !== 0, declaration);
(getModifierFlags(declaration) & ModifierFlags.Readonly) !== 0, declaration);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

result.valueDeclaration = declarations[0];
}
result.isReadonly = isReadonlySymbol(leftProp) || isReadonlySymbol(rightProp);
members[leftProp.name] = result;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

checkIndexConstraints(type);
checkTypeForDuplicateIndexSignatures(node);
checkObjectTypeForDuplicateDeclarations(node);
if (type.flags & TypeFlags.Object) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left over from when { ...T } syntax would produce a spread type. Removed.

result.containingType = containingType;
result.hasNonUniformType = hasNonUniformType;
result.isPartial = isPartial;
result.declarations = declarations;
if (declarations.length) {
result.valueDeclaration = declarations[0];
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking index constraints in checkIndexConstraints relies on valueDeclaration. It's not needed now that we don't create synthetic symbols for spread any more, so I removed it. But in general checkIndexConstraints works on synthetic properties by mistake. If choosing an arbitrary valueDeclaration isn't right, the checkIndexConstraints needs to stop relying on it.

result.declarations = declarations;
if (declarations.length) {
result.valueDeclaration = declarations[0];
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Removed.

@@ -17140,7 +17270,7 @@ namespace ts {
// perform property check if property or indexer is declared in 'type'
// this allows to rule out cases when both property and indexer are inherited from the base class
let errorNode: Node;
if (prop.valueDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
if (prop.valueDeclaration.name.kind === SyntaxKind.ComputedPropertyName || prop.parent === containingType.symbol) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

const name = prop.name;
if (name.kind === SyntaxKind.ComputedPropertyName) {
if (name && name.kind === SyntaxKind.ComputedPropertyName) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore. Removed.

@sandersn
Copy link
Member Author

sandersn commented Nov 7, 2016

I just noticed that I forgot to comment on not allowing intersections. Here's the comment copied out of the original thread since github's UI collapses that one.

Did you consider permitting intersection types as well? I understand how union types are harder and would require "lifting" to produce a resulting union type, but it seems like intersections would just work.

I did, and it would be simple to restore the code I had there earlier. The reason I decided against is that it gets harder to issue a correct, understandable error in checkObjectLiteral because intersections that contain type parameters are still not allowed. And I assume that we'll have a good story for all kinds of type operators shortly after 2.1 ships, so a restriction on intersections won't have lasting problems.

Previously array destructuring inside an object destructuring with an
object rest would downlevel the array destructuring to ES5. This breaks
if the code that targets ES2015 is using iterators instead of arrays
since iterators don't support [0] or .slice that the ES5 emit uses.
Now with object destructuring inside array destructuring inside object
destructuring! Each with their own array/object rest!

Also updates baselines.
@@ -4512,7 +4523,7 @@ namespace ts {
t;
}

function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: string): Symbol {
function createUnionOrIntersectionProperty(containingType: UnionOrIntersectionType, name: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the return type annotation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason. Fixed.

}
if (leftProp.name in members) {
const rightProp = members[leftProp.name];
if (rightProp.flags & SymbolFlags.Optional) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a more correct check here is whether the type of rightProp includes undefined. After all, that's how it works at runtime.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I had to keep the optionality check for when strict null checks is off.

@sandersn sandersn merged commit be5e5fb into master Nov 10, 2016
@wclr
Copy link

wclr commented Nov 22, 2016

Is it possible to omit a property from object?

@MathieuLorber
Copy link

MathieuLorber commented Nov 22, 2016

@whitecolor no, compiler complains

Thanks a lot for that feature, it at least saved my life \o/

@alitaheri
Copy link

@whitecolor

you mean:

const myObj = {a: 1, b:2, c:3};
const { a, ...myObjWithoutA} = myObj;

// myObjWithoutA has b and c but not a

@wclr
Copy link

wclr commented Nov 22, 2016

@alitaheri that is intresting btw, thanks

What I meant a little bit different, though I probably do not refere to spread,

I wanted to make it with only types without objects

say from interface {a: string, b: string} get => interface {b: string}

@MathieuLorber
Copy link

MathieuLorber commented Nov 22, 2016

@whitecolor it isn't relative to spread, from what I understand now. I first understood your question was relative to smthg like that :

interface FooBar {
    foo: string
    bar: string
}
let val = {foo: 'hello'};
let val2: FooBar = {...val};

In that configuration, compiler complains.

@uxp
Copy link

uxp commented Dec 22, 2016

I was looking to implement the emitted code from this patch into my project as a 'backport' utility until we can update to a released Typescript version that contains this operator, but I think I'm not understanding exactly what the patch does, because the way it seems to be implemented might have a bug?

Basically, if we look at the emitted code in isolation, the idea of what it's supposed to do is fairly obvious.

 +        const restHelper = `
 +var __rest = (this && this.__rest) || function (s, e) {
 +    var t = {};
 +    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && !e.indexOf(p))
 +        t[p] = s[p];
 +    return t;
 +};`;

Create new target object t, and for every owned property p of source s that is not specified as excluded in e, copy s's property p to t, and then return target object t.

The only problem with this specifically is that javascript arrays are zero indexed, and the value of the expression !0 is true, so a literal invocation of this function with a source object of { a: 1, b: 2, c: 3, d: 4, e: 5 } such as:

let obj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
let { a, b, c } = obj;
let rest = __rest(obj, ['a', 'b', 'c']);

would result in the rest variable containing the values { a: 1, d: 4, e: 5 }. I would expect the result to lack the keys a, b, and c, but it manages to include a, since it's index in the parameter array e is not greater than zero. In order to accomplish this desire, we could check to see if the index of the excluded key is -1, as that is the value that is returned from the indexOf() function when the value does not exist.

Unless I'm (likely) missing some essential context, it seems to me that there is an issue with the accepted solution.

Thanks for your time.

@normalser
Copy link

normalser commented Dec 22, 2016

@uxp - works fine in latest / playground #12956 FYI

@sandersn sandersn mentioned this pull request Jan 4, 2017
@mhegazy mhegazy deleted the object-spread branch November 2, 2017 21:03
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support proposed ES Rest/Spread properties