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

Strict object literal assignment checking #3823

Merged
merged 23 commits into from
Jul 21, 2015
Merged

Conversation

ahejlsberg
Copy link
Member

This PR implements stricter object literal assignment checks for the purpose of catching excess or misspelled properties. The PR implements the suggestions in #3755. Specifically:

  • Every object literal is initially considered "fresh".
  • When a fresh object literal is assigned to a variable or passed for a parameter of a non-empty target type, it is an error for the object literal to specify properties that don't exist in the target type.
  • Freshness disappears in a type assertion or when the type of an object literal is widened.

Some examples:

var x: { foo: number };
x = { foo: 1, baz: 2 };  // Error, excess property `baz`

var y: { foo: number, bar?: number };
y = { foo: 1, baz: 2 };  // Error, excess or misspelled property `baz`

The rationale for the errors above is that since the fresh types of the object literals are never captured in variables, static knowledge of the excess or misspelled properties should not be silently lost. No errors occur when the fresh types are captured in variables:

var x: { foo: number };
x1 = { foo: 1, baz: 2 };
x = x1;

var y: { foo: number, bar?: number };
y1 = { foo: 1, baz: 2 };
y = y1;

A type can include an index signature to explicitly indicate that excess properties are permitted:

var x: { foo: number, [x: string]: any };
x = { foo: 1, baz: 2 };  // Ok, `baz` matched by index signature

UPDATE 1: This PR also tightens certain parts of the implementation of union types. Specifically:

  • We no longer perform subtype reduction on union types but instead perform a non-lossy deduplication of the constituent type set.
  • The process for determining call and constuct signatures of a union type is now less restrictive.
  • Similar to intersection types, union types now preserve order of the constituent types.

These changes are detailed in the comment thread below.

UPDATE 2: The -suppressExcessPropertyErrors compiler option can be used to disable strict object literal assignment checking. See #4484.

}

class ActionB extends Action {
trueNess: boolean;
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 this should be trueness -- it doesn't look like this test was intended to fail. Another 🏆 for this change's effectiveness.

Copy link

Choose a reason for hiding this comment

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

@RyanCavanaugh
Copy link
Member

👍 . Pulled it down and tried it out for a while -- behavior seems perfect.

I was trying to find an idiomatic way to remove freshness without casting to any. Going through function identity<T>(x: T): T works but has some runtime impact. Any ideas?

@ahejlsberg
Copy link
Member Author

The only way to remove freshness that doesn't generate additional code is a type assertion. I think this is consistent with other situations in which you want to override the type checker. I'm not sure there is a need for a special idiom to defeat the checks associated with freshness.

@JsonFreeman
Copy link
Contributor

I agree that a type assertion is a reasonable escape hatch. You also do not have to cast to any, you can cast to a better type if you have one.

@@ -1743,10 +1743,12 @@ namespace ts {
FromSignature = 0x00040000, // Created for signature assignment check
ObjectLiteral = 0x00080000, // Originates in an object literal
/* @internal */
ContainsUndefinedOrNull = 0x00100000, // Type is or contains Undefined or Null type
FreshObjectLiteral = 0x00100000, // Fresh object literal type
Copy link
Contributor

Choose a reason for hiding this comment

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

Please define fresh object literal in the comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a new flag here? This is set in the same place as ObjectLiteral. And when you get the regular type, you turn off FreshObjectLiteral, but not ObjectLiteral, and I don't really understand why. So a regular type is allowed to have excess properties, but it still does not need to have all the optional properties of the target in a subtype check?

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 related to the issue of removing freshness after the first assignment, but still remembering that the source was an object literal. It may be that we can combine the two if we give up on the first assignment bit.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, I think we should give up the first assignment bit. I think it is a strange rule.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, I don't think we can get rid of the new flag. We use TypeFlag.ObjectLiteral to indicate that a type originated in an object literal and may contain null or undefined types. We can't turn off that flag unless we also widen the type. Yet, in type assertions and during subtype reduction we want to turn off freshness but not widen.

That said, we could still drop the assignment rule. They're really orthogonal issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, let's drop the assignment rule.

I understand what you mean about TypeFlags.ObjectLiteral, but I still think we can use it. We actually use ContainsUndefinedOrNull and ContainsObjectLiteral for what you are talking about, not ObjectLiteral directly. For type assertions, I think it's fine to widen, we already do in the downcast direction, and I think it's fine to do it for the upcast (it uses assignability so we should be fine). For creation of a union type, we already give it TypeFlags.Union plus all the widening flags (which do not include ObjectLiteral anyway). So all the flags that getWidenedType checks for would still be there.

So I think it is safe to remove it.

Copy link
Contributor

Choose a reason for hiding this comment

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

To elaborate about the type assertion case, widening would essentially do two things:

  1. null and undefined become any. They are bottom types anyway, so changing them to any should have no effect.
  2. The object literal is changed with respect to the optional-properties-being-required rule. This rule only applies to subtype, and type assertions use assignability.

Copy link
Contributor

Choose a reason for hiding this comment

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

I admit it's still possible that I missed something, but if I did, I'd like to understand it before allowing us to have two flags that sound really similar and are easy to confuse.

@JsonFreeman
Copy link
Contributor

When we discussed this, we had some thoughts about indexers. Namely if the object type has an indexer, we want to allow excess properties to be provided, since the object might behave as a map. What is the reason for abandoning that idea? Is this going to limit the usefulness of indexers with object literals?

@JsonFreeman
Copy link
Contributor

Where is it that an object literal type loses its freshness? I don't see any place in the code where inferring an object type for a variable causes us to infer the regular type.

To clarify, I would assume this "freshness" gets lost when we widen an object literal type. This is what we do to indicate for example that for subtype, optional properties in the target must be declared in the source, unless the source is a "fresh" (unwidened) object literal. So I am looking for similar logic here.

@JsonFreeman
Copy link
Contributor

I got my indexer question answered.

var resolved = resolveStructuredTypeMembers(type);
return !!(resolved.properties.length === 0 ||
resolved.stringIndexType ||
resolved.numberIndexType ||
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, so you do these checks on the target side as opposed to when contextually typing the object literal. Actually, this is probably a good thing, because not every assignability check is guaranteed to be accompanied by contextual typing. So if the object did not get contextually typed, the assignment would still be allowed, which is good I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

But I would add some comments explaining the rationale behind these heuristics.

// Above we check for excess properties with respect to the entire target type. When union
// and intersection types are further deconstructed on the target side, we don't want to
// make the check again (as it might fail for a partial target type). Therefore we obtain
// the regular source type and proceed with that.
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. This makes sense now

@danquirk
Copy link
Member

Note this is a breaking change. We should probably put this in a widely distributed beta before unleashing it as a default in 1.6 or some future version.

@ahejlsberg
Copy link
Member Author

@danquirk Agreed.

@sophiajt
Copy link
Contributor

We talked about this a bit today in-person. I'm not sure I'm keen on co-opting object literals to mean this, but I think my real discomfort is that, ideally, interfaces should be able to describe any type. They're the fundamental building block of the type system*.

Because of this, I think the natural place is to describe this first in the interface first. Strictness is opt-in there, and would not break code.

I get that there is a lot of interest in putting this in and catching bugs, but I'm not yet seeing the advantage of taking the breaking change over our philosophy of "interface is the foundation of the type system"

That said, I could be swayed...

  • - there are a couple corner cases where this isn't true, but arguably we may want to fix those over time.

mhevery added a commit to mhevery/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
mhevery added a commit to angular/angular that referenced this pull request Aug 13, 2015
BREAKING CHANGE:

Reasons:

1) Interfaces should not start with letter ‘I’
2) Interfaces do not help with mistype properties, but literal types do.
   - microsoft/TypeScript#3823
   - https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#strict-object-literal-assignment-checking
robertknight added a commit to robertknight/passcards that referenced this pull request Aug 16, 2015
TS 1.6 requires that object literals assigned
to variables or parameters with an interface typing
may only contain properties that are listed in the interface.

See microsoft/TypeScript#3823
for details.

This mostly affected React component props interfaces
which did not extend react.Props so were missing the common
key, ref and children props.
@kitsonk kitsonk mentioned this pull request Dec 20, 2015
@microsoft microsoft locked and limited conversation to collaborators Jul 31, 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.