-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Conversation
} | ||
|
||
class ActionB extends Action { | ||
trueNess: boolean; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 . 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 |
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. |
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- null and undefined become any. They are bottom types anyway, so changing them to any should have no effect.
- 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.
There was a problem hiding this comment.
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.
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? |
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. |
I got my indexer question answered. |
var resolved = resolveStructuredTypeMembers(type); | ||
return !!(resolved.properties.length === 0 || | ||
resolved.stringIndexType || | ||
resolved.numberIndexType || |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
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. |
@danquirk Agreed. |
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...
|
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
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
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
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
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
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
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
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.
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:
Some examples:
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:
A type can include an index signature to explicitly indicate that excess properties are permitted:
UPDATE 1: This PR also tightens certain parts of the implementation of union types. Specifically:
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.