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

Conditional type simplifications & Globally cached conditional type instances #29437

Merged
merged 18 commits into from
Mar 8, 2019

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Jan 16, 2019

Fixes #28748

Conditional types of the form T extends U ? T : never simplify to T & U if T always extends U, and never if T can never extend U.

Conditional types of the form T extends U ? never : T simplify to T (T & not U to be more precise, once those are available) if T never extends U, and never if T always extends U.

@jack-williams
Copy link
Collaborator

Does this mean Extract<number | string, any> ~~> any?

@@ -9982,6 +9982,26 @@ namespace ts {
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
// Simplifications for types of the form `T extends U ? U : never` and `T extends U ? never : T`.
Copy link
Member

Choose a reason for hiding this comment

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

First form should be T extends U ? T : never.

// Simplifications for types of the form `T extends U ? U : never` and `T extends U ? never : T`.
const originalCheckType = getActualTypeVariable(root.checkType);
const trueType = instantiateType(root.trueType, mapper);
const falseType = instantiateType(root.falseType, mapper);
Copy link
Member

Choose a reason for hiding this comment

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

These instantiations were always deferred before. Now they're eager. I'm somewhat concerned this creates extra work that we don't need to do.

Copy link
Member Author

@weswigham weswigham Jan 16, 2019

Choose a reason for hiding this comment

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

I could defer them to only when simplified again, but then only types with a declared never would be simplifiable, whereas like this one branch can be instantiated to never, allowing a broader set of conditional types to be reduced.

if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(extendsType))) { // Always true
return getIntersectionType([trueType, extendsType]);
}
else if (getUnionType([getIntersectionType([checkType, extendsType]), neverType]).flags & TypeFlags.Never) { // Always false
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this check the permissive form of the check and extends types? I'm not really following the logic here.

Copy link
Member Author

@weswigham weswigham Jan 16, 2019

Choose a reason for hiding this comment

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

So, the permissive form check returns true for X extends never, while this version of the check does not. We want to ignore the never extends never case here, since that will become a never result in both branches anyway (meaning we'd like x extends never or x extends something instantiable to never to simplify even when the permissive instantiation says there's technically one instantiatiom which'd give another result).

With negated types in place, I'd probably write the check as isTypeAssignableTo(checkType, getNegatedType(extendsType)) instead, but without that in place, this accomplishes the same thing when only normal types exist.

@weswigham
Copy link
Member Author

weswigham commented Jan 16, 2019

Does this mean Extract<number | string, any> ~~> any?

Yeah - number | string extends any is trivially true, then we produce (number | string) & any which just becomes any. Specifically in the extends any case we maybe want to avoid making the intersection to avoid deleting information? We do this when calculating conditional constraints internally already and they should probably match (this is just a simplification to the true branch constraint, after all) - I'll actually go extract the logic and reuse it.

@weswigham
Copy link
Member Author

I'll actually go extract the logic and reuse it.

Done~ extends any (and now things that instantiate to any) no longer create any's in conditional type true branches (so they behave more like the unknown they're acting as).

@weswigham
Copy link
Member Author

@ahejlsberg I've run this on user, RWC, and DT suites - there's no visible adverse effects. I think this is solid.

@RyanCavanaugh
Copy link
Member

Split the bugfix part into a separate PR for 3.3 👍

@RyanCavanaugh
Copy link
Member

And rename a thing

@fatcerberus
Copy link

(number | string) & any which just becomes any

...wait, why? Intuitively I expect that to simplify to number | string. any is a really strange type isn’t it...

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 28, 2019

@fatcerberus It is best not to think of any as a type, but a set of types, IMO. I think current literature would have (number | string) & any be irreducible (up to union-intersection distribution), but TypeScript predates that, and also has practical considerations. Reducing to any is probably the most sensible and conservative choice.

@weswigham weswigham changed the title Conditional type simplifications & Restrictive Instantiation Fix Conditional type simplifications & Globally cached conditional type instances Jan 31, 2019
@weswigham
Copy link
Member Author

@typescript-bot test this & @typescript-bot run dt

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 2, 2019

Heya @weswigham, I've started to run the Definitely Typed test suite on this PR at 47fbc47. You can monitor the build here. It should now contribute to this PR's status checks.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 2, 2019

Heya @weswigham, I've started to run the extended test suite on this PR at 47fbc47. You can monitor the build here. It should now contribute to this PR's status checks.

@weswigham
Copy link
Member Author

@typescript-bot run dt - I just made some changes after running into a manifestation of #30152 with the new simplifications on DT and included a small workaround.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 7, 2019

Heya @weswigham, I've started to run the Definitely Typed test suite on this PR at 8268eeb. You can monitor the build here. It should now contribute to this PR's status checks.

@weswigham
Copy link
Member Author

Sweet, the only change on DT is now a NodePath<Identifier> -> NodePath<{ type: "Identifier"; } & Identifier> in babel's types inline with the baseline changes in this PR (which if supertype reduction was a thing would be a noop), so DT looks super clean now.

@ahejlsberg can we look at merging this again? #28748 is a pretty popular issue to fix and the global caching of conditionals should be a nice perf win.

Copy link
Member

@ahejlsberg ahejlsberg left a comment

Choose a reason for hiding this comment

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

Add a getConditionalTypeWorker that doesn't have to worry about caching.

@weswigham
Copy link
Member Author

@ahejlsberg done.

@jcalz
Copy link
Contributor

jcalz commented Mar 22, 2019

It's too bad that intersections of known concrete object types don't reduce to their mapped identity type automatically (e.g., {a: string} & {b: number} is not automatically collapsed to {a: string, b: number}), since this new behavior is going to take Extract<T, U> and turn it from a simple filtering of a union into a gnarly union-of-intersections. I suppose one could get back something that looks like the old behavior via

type OldExtract<T, U> = T extends U ? { [K in keyof T]: T[K] } : never

maybe?


I'm also not sure how T & U is simpler than T if T extends U is known to be true. If T extends U then T & U should be equivalent to T (except that the compiler probably won't notice it and will end up carrying around these vestigial intersections). What am I missing? Can someone give me an example of a type where T extends U ? true : false is true but T & U is not just a more complicated way of expressing T?

@Jessidhia
Copy link

Jessidhia commented Mar 22, 2019

{} extends { a?: boolean } ? true : false is true, but {} & { a?: boolean } is not the same as just {}

(this is not a full answer, just a counter-example)

@jcalz
Copy link
Contributor

jcalz commented Mar 22, 2019

Yeah, I'll buy that... optional properties are weird. (The value {a: 1} is assignable to type {} but not to type {a?: boolean}, so it's odd that {} extends {a?: boolean} is seen as true.)

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.

8 participants