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

Use type parameter constraints for computed property types #17404

Merged
merged 10 commits into from
Aug 8, 2017

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Jul 25, 2017

Fixes #14473
Fixes #15501
Fixes #17069

Note this code calls getBaseConstraintOfType the same way that getApparentType does. Calling getApparentType does not work here because it converts string to String and so on.

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.

@sandersn Is there a test for T extends number as well? It's probably the only thing that'd be nice to have if not present, since I think that would produce one fewer errors now? (like the changed U extends string test)

Aside from that remark, this looks like a relatively neat change. There might be a more general way to solve this and some of our other suggestions about things like intersection types and computed property names, but I think this is probably fine for this issue, and can be expanded upon later if need be.

@ahejlsberg
Copy link
Member

ahejlsberg commented Jul 25, 2017

Instead of this targeted change, it might make more sense to review all the uses of isTypeAnyOrAllConstituentTypesHaveKind function and replace them with calls to isTypeAssignableTo as appropriate. We have other similar issues, such as not being able to use arithmetic operators with T extends number values, and we might as well fix them all in one go.

This improves a number of error messages to do with adding 'null' or
'undefined' with other types. It also allows you to add two type
parameters that both extend `number`.
@sandersn
Copy link
Member Author

I replaced the body of isTypeOfKind (which isTypeAnyOrAllConstituentsTypesHaveKind immediately delegates to). The tests are passing but I still want to improve the any/null/undefined/void exclusion code since it's really the caller's responsibility.

@sandersn
Copy link
Member Author

Note that this most recent commit improves the error message for expressions like null + undefined or null + 1 considerably, in addition to allowing this:

function f<T extends number>(t: T) {
  return t + t
}

Here, the return type of f is number. That's still hard-coded in the checker code for binary expressions.

return true;
}
}
const targets = [];
Copy link
Member

Choose a reason for hiding this comment

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

Why are you building up a union type here? It causes allocations and ends up being broken down to discrete checks for each contained type anyway. I think it would be better to just have a sequence of conditional isTypeAssignableTo calls here, e.g.

return kind & TypeFlags.NumberLike && isTypeAssignableTo(source, numberType) ||
    kind & TypeFlags.StringLike && isTypeAssignableTo(source, stringType) ||
    kind & TypeFlags.BooleanLike && isTypeAssignableTo(source, BooleanType) ||
    ...

Copy link
Member Author

@sandersn sandersn Aug 2, 2017

Choose a reason for hiding this comment

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

var p1: number | string
var o = {
  [p1]() { }
//~~~~ A computed property name must be of type 'string', 'number', ...
}

But I think I can fix that by adding a fallback to isTypeAssignableTo the union type in just the case of computed properties.

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. The computed property name type check now has a fallback call to isTypeAssignableTo(links.resolvedType, getUnionType([numberType, stringType, esSymbolType]). Everything else is fine with a sequence of isTypeAssignableTo calls.

// of the given kind.
function isTypeOfKind(type: Type, kind: TypeFlags): boolean {
if (type.flags & kind) {
function isTypeOfKind(source: Type, kind: TypeFlags, excludeAny?: 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'm not sure I get why we need this excludeAny parameter?

Copy link
Member Author

Choose a reason for hiding this comment

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

checkBinaryLikeExpression has a complicated way of checking legal types for +:

  1. It disallows null + number and number + undefined, even with strictNullChecks off. I like this check and want to keep it.
  2. It gives a different types for string + any ==> string vs T + any ==> any and T + unknown ==> unknown.

It does this with the following (simplified) structure (using the abbreviation tak for isTypeAssignableToKind):

if (tak(left, NumberLike) && tak(right, NumberLike)) {
  return numberType;
}
else if (tak(left, StringLike) || tak(right, StringLike)) {
  return stringType;
}
else if (isTypeAny(left) || isTypeAny(right)) {
  return anyType;
}
else {
  returned undefined; // signal an error
}

Without excludeAny (now called strict), isTypeAssignableTo(number, undefined) is true, and number + undefined/null is now legal. Also, isTypeAssignableTo(number, any) is true, and number + any ==> number , and any + any ==> number.

I tried a few things to pull the any checks before the calls to isTypeAssignableToKind, but was not successful. If you see a way, let me know.

@ahejlsberg
Copy link
Member

@sandersn With the latest changes this also fixes #15501 and #17069.

@simonbuchan
Copy link

The errors also sound similar to the string enums as keys errors, eg #16687, #16760?

@sandersn
Copy link
Member Author

sandersn commented Aug 2, 2017

@simonbuchan Your example #16687 has the computed property name error as a side effect, but this fix doesn't provide the type that you're looking for there.

@sandersn
Copy link
Member Author

sandersn commented Aug 2, 2017

This PR is ready to look at again.

}
return false;
return kind & TypeFlags.NumberLike && isTypeAssignableTo(source, numberType) ||
Copy link
Member

Choose a reason for hiding this comment

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

Calling isTypeAssignableTo so many times seems fairly expensive if source is not a simple type. While the assignability check has a fast path for simple types via isSimpleTypeRelatedTo, if the relation doesn't hold we fall back to the more complex checkTypeRelatedTo function. I would recommend you determine if there is any additional check time overhead with this approach.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also note that calls to isTypeAssignableToKind only ever pass 1-3 flags in, with 1 being the most common. So the expected perf hit would be just from calling isTypeAssignableTo once compared to the old logic.

@sandersn
Copy link
Member Author

sandersn commented Aug 8, 2017

Sure, I'll test it, although @ahejlsberg was confident that his changes to isTypeAssignableTo would make this cheap enough.

@sandersn
Copy link
Member Author

sandersn commented Aug 8, 2017

Here are the ts-perf results (angular doesn't build on my installation of ts-perf). Total time goes down a tiny amount. Check time went up by 0.01s for TFS, and down 0.01s for Monaco. So I don't think there's any performance impact with this change.

┌─────────────┬─────────────────────┬─────────────────────┬───────────────────┬───────────┬────────────┐
│ Project     │            Baseline │             Current │             Delta │      Best │      Worst │
╞═════════════╧═════════════════════╧═════════════════════╧═══════════════════╧═══════════╧════════════╡
│ Monaco - node (v8.1.2, x64)                                                                          │
├─────────────┬─────────────────────┬─────────────────────┬───────────────────┬───────────┬────────────┤
│ Memory used │ 366,008k (±  0.00%) │ 366,041k (±  0.01%) │   +33k (+  0.01%) │  365,987k │   366,174k │
│ Parse Time  │    1.25s (±  0.70%) │    1.24s (±  0.28%) │ -0.00s (-  0.28%) │     1.23s │      1.25s │
│ Bind Time   │    0.60s (±  0.40%) │    0.60s (±  0.81%) │ +0.00s (+  0.75%) │     0.59s │      0.64s │
│ Check Time  │    3.10s (±  1.53%) │    3.09s (±  0.26%) │ -0.01s (-  0.48%) │     3.06s │      3.13s │
│ Emit Time   │    1.87s (±  0.85%) │    1.87s (±  0.48%) │ -0.00s (-  0.03%) │     1.84s │      1.91s │
├─────────────┼─────────────────────┼─────────────────────┼───────────────────┼───────────┼────────────┤
│ Total Time  │    6.82s (±  0.89%) │    6.80s (±  0.23%) │ -0.02s (-  0.25%) │     6.77s │      6.89s │
╞═════════════╧═════════════════════╧═════════════════════╧═══════════════════╧═══════════╧════════════╡
│ TFS - node (v8.1.2, x64)                                                                             │
├─────────────┬─────────────────────┬─────────────────────┬───────────────────┬───────────┬────────────┤
│ Memory used │ 319,284k (±  0.00%) │ 319,305k (±  0.00%) │   +21k (+  0.01%) │  319,282k │   319,363k │
│ Parse Time  │    0.91s (±  0.74%) │    0.91s (±  0.34%) │ -0.00s (-  0.16%) │     0.90s │      0.92s │
│ Bind Time   │    0.49s (±  3.23%) │    0.49s (±  2.17%) │ -0.01s (-  1.22%) │     0.47s │      0.58s │
│ Check Time  │    2.75s (±  0.94%) │    2.76s (±  0.81%) │ +0.01s (+  0.44%) │     2.58s │      2.83s │
│ Emit Time   │    1.77s (±  1.12%) │    1.76s (±  0.27%) │ -0.01s (-  0.82%) │     1.74s │      1.77s │
├─────────────┼─────────────────────┼─────────────────────┼───────────────────┼───────────┼────────────┤
│ Total Time  │    5.92s (±  0.36%) │    5.91s (±  0.22%) │ -0.01s (-  0.19%) │     5.84s │      5.95s │
└─────────────┴─────────────────────┴─────────────────────┴───────────────────┴───────────┴────────────┘

Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

One minor comment, though I would probably wait to see if @ahejlsberg has any other further comments before merging.

@@ -17073,7 +17061,7 @@ namespace ts {
// and the right operand to be of type Any, a subtype of the 'Function' interface type, or have a call or construct signature.
// The result is always of the Boolean primitive type.
// NOTE: do not raise error if leftType is unknown as related error was already reported
if (isTypeOfKind(leftType, TypeFlags.Primitive)) {
if (!(leftType.flags & TypeFlags.Any) && isTypeAssignableToKind(leftType, TypeFlags.Primitive)) {
Copy link
Member

Choose a reason for hiding this comment

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

We have an isTypeAny that is a bit more readable than the bitmask, e.g. !isTypeAny(leftType) && isTypeAssignableToKind(leftType, TypeFlags.Primitive)

@sandersn
Copy link
Member Author

sandersn commented Aug 8, 2017

I'd like to get this into 2.5 RC so people can catch any lurking bugs. I'm on vacation starting Thursday, so let's leave this open until Friday to see if @ahejlsberg has a chance to look at this while he's gone too. I'll leave it up to you to merge or not at that point. @rbuckton.

I'll also try running ts-perf on a stress-test scenario that has lots of object type + string expressions.

@rbuckton
Copy link
Member

rbuckton commented Aug 8, 2017

I can see how isTypeAssignableTo has the same or better performance when the source type is simple enough (e.g. already a number, string, enum, or union as would generally be the case for our perf tests which have mostly correct code), but if the source type is a structured type or a type variable we end up in recursiveTypeRelatedTo which has 3 array allocations per each call to isTypeAssignableTo and will inflate the size of the assignability cache by 9 entries.

This may be acceptable overhead for cases where we are likely to fail, but something we should consider. It may be worthwhile for us to compose a performance benchmark against a scenario we tailor to be filled with incorrect code so that we can watch for performance regressions in those cases.

@rbuckton
Copy link
Member

rbuckton commented Aug 8, 2017

After reviewing this further with @sandersn, I'm not too concerned about the performance since we don't dig into target as it is a simple type. 👍

@sandersn sandersn merged commit 847d7fe into master Aug 8, 2017
@sandersn sandersn deleted the use-type-param-constraints-for-computed-prop-types branch August 8, 2017 23:29
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 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.

6 participants