-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Lazily compute signature type predicates #17600
Conversation
For posterity, the test I was using to reproduce #17451 was:
|
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 like the delay -- it should have always been this way to mirror the way return types are treated -- but I wish there were a more uniform way to represent 'pending' vs 'missing'. The problem is that for efficiency, the common case should be 'undefined', which for type predicates is 'missing' and for return types is 'pending'.
src/compiler/checker.ts
Outdated
} | ||
|
||
function getTypePredicateOfSignature(signature: Signature): TypePredicate { | ||
if (signature.resolvedTypePredicate === "pending") { |
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 third state when resolvedReturnType doesn't?
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.
oh, because all signatures have a return type (eventually), but not all signatures will have a typePredicate. So "pending"
for resolvedReturnType
is the equivalent of undefined
for resolvedTypePredicate
.
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.
oh, this is documented in the definition in types.ts
src/compiler/checker.ts
Outdated
@@ -5434,14 +5435,14 @@ namespace ts { | |||
} | |||
|
|||
function createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], thisParameter: Symbol | undefined, parameters: Symbol[], | |||
resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasLiteralTypes: boolean): Signature { | |||
resolvedReturnType: Type | undefined, resolvedTypePredicate: TypePredicate | undefined | "pending", minArgumentCount: number, hasRestParameter: boolean, hasLiteralTypes: boolean): Signature { |
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 be aware that if we call createSignature
once with a TypePredicate
and then call createSignature
again later with "pending"
that it will result in a V8 compiling a second copy of createSignature
due to the parameter being polymorphic (string vs. object).
We also end up making Signature
more polymorphic. This could result in degraded performance so I would recommend you run benchmarks before and after this change to ensure this does not cause a regression.
As an alternative, I would suggest that you define something like a const unresolvedTypePredicate = <IdentifierTypePredicate>{ kind: TypePredicateKind.Identifier, parameterName: "pending", parameterIndex: 0 };
to act as your "pending"
sentinel value as it will reduce polymorphism.
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.
Could we just use null
for pending?
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.
We try not to use null in the compiler at all, and it would be inconsistent.
src/compiler/checker.ts
Outdated
@@ -280,6 +280,11 @@ namespace ts { | |||
const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); | |||
const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*typePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); | |||
|
|||
const unresolvedTypePredicate: void & { __unresolvedTypePredicate: void } = (() => { |
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 is this type so needlessly complex? Just use TypePredicate
.
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 want to ensure that this is definitely not useable as a TypePredicate
-- it's just one to prevent polymorphism. Otherwise it's too easy to access resolvedTypePredicate
and think you're getting the right thing.
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.
We generally don't have that issue with other cases like noConstraintType
, especially if all access to the type predicate is gated through getTypePredicateOfSignature
.
While I don't think it's strictly necessary, you could add another TypePredicateKind as a discriminant, but I think using an object whose shape (and hidden class) matches other valid values for that property will reduce polymorphism.
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 would disagree that it's not an issue -- it might not be an issue if you wrote the code itself, but coming from the outside, this was a barrier to my understanding this code, and it would have been easier had I realized that resolvedReturnType
was not meant to be used directly as a Type
. I hadn't come across noConstraintType
yet, but I'm sure it wouldn't have been easy to realize that a variable of type Type
should not actually be used as a Type
because it might be a special sentinel value.
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.
This one of many cases where we need better documentation in checker.
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 would prefer we stick to our current pattern for these cases. We can discuss these specific concerns with the broader team following the 2.5 release.
src/compiler/checker.ts
Outdated
@@ -11403,7 +11423,7 @@ namespace ts { | |||
const apparentType = getApparentType(funcType); | |||
if (apparentType !== unknownType) { | |||
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call); | |||
return !!forEach(callSignatures, sig => sig.typePredicate); | |||
return some(callSignatures, sig => signatureHasTypePredicate(sig)); |
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.
Can you just use signatureHasTypePredicate
directly instead?
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.
👍
src/compiler/checker.ts
Outdated
return signature.resolvedTypePredicate !== undefined; | ||
} | ||
|
||
function getTypePredicateOfSignature(signature: Signature): TypePredicate | undefined { |
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.
Another approach we could consider is the one used to resolve Type Parameter constraints. Instead of using undefined
to indicate no type predicate and an unresolvedTypePredicate
sentinel to indicate deferred resolution, use undefined
to indicate deferred resolution and a noTypePredicate
sentinel. See noConstraintType
as an example.
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.
Shouldn't undefined
be used for the most common case though? Usually no type predicate exists.
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.
It can also mean that we don't know the answer yet. This is consistent with other "resolve"-named properties used in checker.
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've inverted the representation, so undefined
now represents a lazily-computed type predicate and noTypePredicate
represents no type predicate existing.
Can you take a look at the build failures? |
…edicate` instead of `undefined` when in all `createSignature` calls
@rbuckton @ahejlsberg Any more comments? |
Bump -- @rbuckton @ahejlsberg Is anything blocking this? |
@rbuckton Good to go? |
src/compiler/checker.ts
Outdated
signature.resolvedTypePredicate = instantiateTypePredicate(getTypePredicateOfSignature(signature.target), signature.mapper); | ||
} | ||
return signature.resolvedTypePredicate as TypePredicate; | ||
} |
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.
Write this function in the same style as getConstraintFromTypeParameter
.
New commit contains fixes to the union half of #17757. Intersecting type predicates is marked as a TODO. |
@sandersn @rbuckton @ahejlsberg Anyone? |
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.
One test assertion in a comment can be checked by the compiler. Otherwise looks good.
@@ -0,0 +1,3 @@ | |||
declare function f<T>(predicate: (x: {}) => x is T): T; | |||
// 'res' should be of type 'number'. |
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.
change this line to var res: number
and start the next line with var res = ...
and the compiler will enforce this assertion.
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 didn't want us taking any contextual type -- presumably we should be doing that on assignments if we're not already.
The baselines do enforce this assertion.
type Or = A | B; | ||
|
||
function f(o: Or, x: {}, y: {}) { | ||
if (o.pred(x, y)) { |
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.
interesting.. why doesn't this match?
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.
oh, it's because you can't make a single type predicate that narrows two variables.
src/compiler/types.ts
Outdated
// Lazily set by `getTypePredicateOfSignature`. | ||
// `undefined` indicates a type predicate that has not yet been computed. | ||
// Uses a special `noTypePredicate` sentinel value to indicate that there is no type predicate. This looks like a TypePredicate at runtime to avoid polymorphism. | ||
// (See https://github.com/Microsoft/TypeScript/pull/17600#discussion_r132059173) |
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.
This link is a bet that our use of github will last as long as the typescript source. Probably a decent bet, but I personally think the explanation stands on its own as a valid justification.
(Same for the use of github bug numbers earlier in the review.)
sig.minArgumentCount = minArgumentCount; | ||
sig.hasRestParameter = hasRestParameter; | ||
sig.hasLiteralTypes = hasLiteralTypes; | ||
return sig; | ||
} | ||
|
||
function cloneSignature(sig: Signature): Signature { | ||
return createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, sig.resolvedReturnType, | ||
sig.typePredicate, sig.minArgumentCount, sig.hasRestParameter, sig.hasLiteralTypes); | ||
return createSignature(sig.declaration, sig.typeParameters, sig.thisParameter, sig.parameters, /*resolvedReturnType*/ undefined, |
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.
odd, I thought cloneSignature
already zeroed out resolvedReturnType
.
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.
oh, right, this is from that analysis you did which showed that all callers of cloneSignature immediately zeroed it out themselves.
src/compiler/checker.ts
Outdated
function getUnionTypePredicate(signatures: ReadonlyArray<Signature>): TypePredicate { | ||
let first: TypePredicate | undefined; | ||
const types: Type[] = []; | ||
for (let i = 0; i < signatures.length; i++) { |
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 not for-of here?
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.
If we did #19309 we could enable prefer-for-of
to catch this kind of error.
getIndexTypeOfStructuredType, | ||
getConstraintFromTypeParameter, | ||
getFirstIdentifier, | ||
), |
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.
Did anything change here? Otherwise, just leave it alone.
src/compiler/checker.ts
Outdated
hasRestParameter, | ||
hasLiteralTypes) { | ||
return createSignature(declaration, typeParameters, thisParameter, parameters, resolvedReturnType, resolvedTypePredicate || noTypePredicate, minArgumentCount, hasRestParameter, hasLiteralTypes); | ||
}, |
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 the change here?
const sig = new Signature(checker); | ||
sig.declaration = declaration; | ||
sig.typeParameters = typeParameters; | ||
sig.parameters = parameters; | ||
sig.thisParameter = thisParameter; | ||
sig.resolvedReturnType = resolvedReturnType; | ||
sig.typePredicate = typePredicate; | ||
sig.resolvedTypePredicate = resolvedTypePredicate; |
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 would just do typePredicate || noTypePredicate
here and then you won't need all the other changes.
I just pushed a branch with some suggested changes: https://github.com/Microsoft/TypeScript/tree/typePredicateChanges This makes the |
@ahejlsberg Good to go? |
@andy-ms Also, check to see if this fixes #19676. |
#19676 is fixed, tested for by |
@@ -866,9 +866,9 @@ define(function () { | |||
>'UnknownMobile' : "UnknownMobile" | |||
|
|||
isArray = ('isArray' in Array) ? | |||
>isArray = ('isArray' in Array) ? Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : (arg: any) => arg is any[] | |||
>isArray = ('isArray' in Array) ? Array.isArray : function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : (value: any) => 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 change is correct because only the left hand side of the conditional is a type predicate. @ahejlsberg could you verify, then merge
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.
Something seems fishy here. If you reverse the order of the operands you get the old result. So apparently both types are subtypes of each other, or otherwise subtype reduction would consistently pick one of them. I'm not sure why this is the case, since (x: any) => x is any[]
should be a subtype of (x: any) => boolean
. You may want to investigate.
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.
Added a test -- the return type seems to be boolean
the other way around as well.
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.
Ok, looks good.
I think this PR is good to go. |
Note: This includes a workaround to a type inferring issue fixed by microsoft/TypeScript#17600. When Typescript 2.7 is released, these forms can be removed.
Fixes #17451
EDIT: And #19640 and #19642 and #20186.
To summarize the problem:
During the first quickInfo, we start from a blank slate.
In
chooseOverload
:excludedArgument
to[true]
, meaning that we exclude the sole argument.{}
.excludeArgument
will beundefined
. This means we enter the body ofcheckFunctionExpressionOrObjectLiteralMethod
; particularly where we callinstantiateSignature(contextualSignature, contextualMapper);
.cloneTypePredicate
. This uses the contextual mapper to call map the contextual type predicate type to the actual type predicate type.mapper
(created bycreateInferenceContext
), we cause a side-effect of fixing the inference -- so it gets stuck at{}
forever.The statefulness in the original issue is because by the second quickInfo, we've already checked the function, and its return type is
(n: {}) => n is number
. The explicit: n is number
annotation overrides the contextual type. SogetTypeOfFuncClassEnumModule
returns a completed full type just an empty new one, and we can infer types correctly. During the first quickInfo we didn't have a type for the function yet, so type inference problems only showed up then.We had already solved this sort of problem for contextual return types: compute them lazily so that we don't call the contextual mapper until necessary. So the solution is simply to do the same for the type predicate.