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

Computed property union lifting #21070

Closed
wants to merge 15 commits into from
Closed

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Jan 8, 2018

Fixes #13948
Fixes #16789
Fixes #20725

Computed properties of object literals with a union type distribute across the type to create a union type. In the following example,

let ab: 'a' | 'b'
let o = { [ab]: 'hi' }

o has the type { a: string } | { b: string }.

Previously, computed properties caused the object literal type to have a index signature instead. This now happens only if the type of the computed property is not a literal type or union of literal types.

This PR also improves two other aspects of computed properties:

  1. Destructuring: Destructuring with a literal-typed computed property (or union-literal-typed) now correctly uses the type of the index signature for the resulting variable:
declare let one: 1
declare let onetwo: 1 | 2
declare let o: { [n: number]: boolean }
declare let o1: { '1': boolean }
var { [one]: d1 } = o1
var { [one]: d1s } = o
var { [onetwo]: d12 } = o

d1, d1s and d12 have the type boolean now, instead of any.

  1. Printing: Computed properties with literal types or unions of literal types now print the string representation of the literal type instead of the source expression. This avoids duplicates from lifted unions, but makes the type more understandable even for other computed properties:
declare let a: 'x'
declare let ab: 'a' | 'b'
let o1 = { [a]: 'hi' }
let o2 = { [ab]: 'hi' }

Currently the quickinfo for o1 is { ["x"]: string }. Previously it was { [a]: string }. Similarly, the quickinfo for o2 is { ["a"]: string } | { ["b"]: string }. Without improved printing it is { [ab]: string } | { [ab]: string }.

@sandersn sandersn requested review from weswigham and mhegazy January 8, 2018 18:10
@sandersn
Copy link
Member Author

sandersn commented Jan 8, 2018

@DanielRosenwasser you sounded interested in this too, although I don't know if you have time for a review right now.

if (!computedType) {
return anyType;
}
else if (computedType.flags & TypeFlags.Literal) {
Copy link
Member

Choose a reason for hiding this comment

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

Both of the following two branches should just be the union and non-union cases under an isLiteralType(computedType) check, no? It already handles unions/enums correctly. Unless you're explicitly excluding literal type unions which include null/undefined (which, IMO, should be handled with getNonNullableType, if that's the case?)

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 didn't think about null/undefined, but it seems like they should be handled the same as other literals. isLiteralType does this correctly.

hasComputedStringProperty = false;
hasComputedNumberProperty = false;
typeFlags = 0;
updateIntermediateType(getSpreadType(intermediate, createObjectLiteralType(), node.symbol, propagatedFlags, /*fromComputedProperty*/ false));
Copy link
Member

Choose a reason for hiding this comment

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

This location no longer does

                        hasComputedStringProperty = false;		
                        hasComputedNumberProperty = false;		
                        typeFlags = 0;

Is that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this was a mistake because it reset these properties between spreads. I thought one of the baselines improved as a result, but I don't see it now. It was something like { [s1]: 1, ...o, [s2]: 2, ...o }

Copy link
Member Author

Choose a reason for hiding this comment

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

computedPropertyNames5_ES5/ES6

else if (computedType.flags & TypeFlags.Union && (computedType as UnionType).types.every(t => !!(t.flags & TypeFlags.Literal))) {
type = isTypeAny(objectLiteralType)
? objectLiteralType
: (computedType as UnionType).types.every(t => !!(t.flags & TypeFlags.NumberLiteral)) && getIndexTypeOfType(objectLiteralType, IndexKind.Number) ||
Copy link
Member

Choose a reason for hiding this comment

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

This code looks suspiciously similar to the above code in getTypeForBindingElement maybe a helper for getTypeOfComputedDestructuredProperty (akin to getTypeOfDestructuredProperty) or something is in order?

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. The resulting function issues errors, so I called it checkComputedDestructuredProperty

}
else {
const text = getTextOfPropertyName(name);
type = isTypeAny(objectLiteralType)
Copy link
Member

Choose a reason for hiding this comment

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

I know you just moved this from below, but I think this could probably be

type = isTypeAny(objectLiteralType) ? objectLiteralType : getTypeOfDestructuredProperty(objectLiteralType, name);
if (type === unknownType) { type = undefined; } // Not sure if this is necessary, but is the only difference in behavior between what was here and `getTypeOfDestructuredProperty`

Copy link
Member Author

Choose a reason for hiding this comment

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

During checking, I'd rather check type flags than parse+serialise the name like isNumericLiteralName does.

@@ -0,0 +1,62 @@
tests/cases/conformance/es6/computedProperties/computedPropertyUnionLiftsToUnionType.ts(32,4): error TS2459: Type '{ a: string; } | { b: string; }' has no property '[ab]' and no string index signature.
Copy link
Member

Choose a reason for hiding this comment

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

has no property '[ab]' and no string index signature - this error seems somewhat lacking. I'd expect either has no property 'a' and no string index signature or has no property 'b' and no string index signature, given the other changes you've made.

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 improved the error reporting and put it behind the noImplicitAny flag where it belongs.

@@ -144,7 +144,7 @@ export default {

=== tests/cases/compiler/comma.ts ===
export default {
>{ ['foo']: 42} : { ['foo']: number; }
>{ ['foo']: 42} : { ["foo"]: number; }
Copy link
Member

Choose a reason for hiding this comment

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

I see this is no longer preserving quotemarks. Does this affect declaration emit? For example, in

const ab: 'a' | 'b' = 'a';
export obj = { [ab]: "quas" };

what is the declaration emit? Is it export obj: { ['a']: string } | { ['b']: string }?
Do we use single quotes on 'a' and 'b', like the code does when the type is made, or no? I know it's best-effort, so I can understand if we don'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.

Actually, it seems to be using a completely different code path, because the output is { a: string } | { b: string }. I guess this is OK, but I didn't expect it. See the latest commits for the declaration baseline.

!((symbol as TransientSymbol).checkFlags & CheckFlags.Late) &&
!isWellKnownSymbolSyntactically((name as ComputedPropertyName).expression) &&
symbol.escapedName) {
return '["' + unescapeLeadingUnderscores(symbol.escapedName) + '"]';
Copy link
Member

Choose a reason for hiding this comment

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

Previously this would print [0] if the computed property originally was a numeric literal. I know this is more correct with how we view the type (since keys are always strings), but it does feel unfortunate to be always quoting everything in the result. Maybe not quoting numeric names here would make me feel better? It would probably also reduce baseline noise.

Copy link
Member Author

Choose a reason for hiding this comment

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

True on all points. It's easy enough to change, so I did. But as you noticed below, we aren't really trying to preserve what the user typed with this change.

symbol.flags & SymbolFlags.Transient &&
!((symbol as TransientSymbol).checkFlags & CheckFlags.Late) &&
!isWellKnownSymbolSyntactically((name as ComputedPropertyName).expression) &&
symbol.escapedName) {
Copy link
Member

Choose a reason for hiding this comment

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

Can the escapedName be the empty string? That would also be falsey here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, { ['']: 1 } is perfectly legal. I added !== undefined here.

@mhegazy
Copy link
Contributor

mhegazy commented Jan 9, 2018

@sandersn this will resolve the issue in #21030, correct?

@Jessidhia
Copy link

Jessidhia commented Jan 9, 2018

IIUC this should also fix #16789

EDIT: hmm, almost. That bug uses a non-literal type so it may not be covered 🤔

@sandersn
Copy link
Member Author

sandersn commented Jan 9, 2018

@Kovensky The new destructuring code was almost enough, I just needed to add a couple of cases. With the latest commit, #16789 is also fixed.

@sandersn
Copy link
Member Author

sandersn commented Jan 9, 2018

@mhegazy No, after in-person discussion we decided that #21030 needs something like the proposed Unionize type.

@sandersn
Copy link
Member Author

sandersn commented Jan 9, 2018

@weswigham I think this is ready for another look.

Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

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

Minor style nit, but otherwise looks good. I'm not the biggest fan of a boolean parameter on getSpreadType, but it'll do.

@@ -4391,22 +4400,28 @@ namespace ts {
// Use explicitly specified property name ({ p: xxx } form), or otherwise the implied name ({ p } form)
const name = declaration.propertyName || <Identifier>declaration.name;
if (isComputedNonLiteralName(name)) {
// computed properties with non-literal names are treated as 'any'
return anyType;
type = checkComputedDestructuredProperty(parentType, name, text => {
Copy link
Member

Choose a reason for hiding this comment

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

I'd write a named function in scope for this lambda and then reuse it below in the else branch.

@mhegazy
Copy link
Contributor

mhegazy commented Jan 10, 2018

The scope of this change is bigger than i previously anticipated. We need to discuss this change in the design meeting.

ebramanti added a commit to VideoAmp/tslint-config-videoamp that referenced this pull request Mar 24, 2018
- Issues with dynamic keys, microsoft/TypeScript#21070
- Issues with React.SFC array return type, microsoft/TypeScript#21699
@typescript-bot
Copy link
Collaborator

Thanks for your contribution. This PR has not been updated in a while and cannot be automatically merged at the time being. For housekeeping purposes we are closing stale PRs. If you'd still like to continue working on this PR, please leave a message and one of the maintainers can reopen it.

@IanKemp
Copy link

IanKemp commented Aug 31, 2018

Is this ever going to be implemented?

@weswigham
Copy link
Member

I may actually be able to integrate fixes for the same issues into #26797. I'll look into it.

@sandersn
Copy link
Member Author

It may also be possible to excise a less-ambitious chunk of this PR. It's on my list of bugs to fix for 3.1 or 3.2, but near the bottom. I'll talk with @weswigham to decide on an approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants