Skip to content

Fix discriminant checking in contextual types #23794

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

Merged
merged 1 commit into from
May 22, 2018

Conversation

ahejlsberg
Copy link
Member

Fixes #23777. Since the repro scenario is somewhat involved there is no regression test in this PR. However, I have manually verified that the issue is fixed for #23777.

@ahejlsberg ahejlsberg requested review from sandersn and mhegazy April 30, 2018 22:46
@ahejlsberg ahejlsberg added this to the TypeScript 2.9 milestone Apr 30, 2018
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.

I don't particularly like the idea of merging a change like this without a test - it's super easy to accidentally regress 😢

case SyntaxKind.TrueKeyword:
case SyntaxKind.FalseKeyword:
case SyntaxKind.NullKeyword:
case SyntaxKind.Identifier:
Copy link
Member

@weswigham weswigham Apr 30, 2018

Choose a reason for hiding this comment

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

An identifier may end up being any type and cause a circularity still (likely indirected through control flow analysis, like the issue in #23661 is) - while this change may happen to break the exact loop in the issue, there's probably still a problem if you add an indirection.

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'm not sure what you mean. The key here is that checkIdentifier will never ask for the contextual type of the identifier and therefore it can't cause a circularity issue.

Copy link
Member

Choose a reason for hiding this comment

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

AFAIK if the Identifier in question is a parameter name it can still be contextually typed - though I suppose that case should be handled outside of checkExpression, and inside getContextualType.

Copy link
Member

@weswigham weswigham May 1, 2018

Choose a reason for hiding this comment

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

Also, literally any expression can be a discriminant here (because discrimination is type-based and not syntactic), see this, which works today and this PR would break:

type A = {
    kind: "a",
    value: 42,
    doer(): void;
}

type B = {
    kind: "b",
    value: string,
    doer(): void;
}

function getMode(): "a" {
    return "a";
}

function needsNumber(x: 42): void { }

const x: A | B = {
    kind: getMode(),
    value: 42,
    doer() {
        needsNumber(this.value);
    }
};

if (isDiscriminantProperty(contextualType, prop.symbol.escapedName)) {
const discriminatingType = getTypeOfNode(prop.initializer);
if (isPossiblyDiscriminantValue(prop.initializer) && isDiscriminantProperty(contextualType, prop.symbol.escapedName)) {
const discriminatingType = checkExpression(prop.initializer);
Copy link
Member

Choose a reason for hiding this comment

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

I think it'd work much more uniformly to just check the initializer with the thus-far-known context, eg: checkExpressionWithContextualType(prop.initializer, getTypeOfPropertyOfContextualType(contextualType, prop.symbol.escapedName), cloneTypeMapper(identityMapper)), rather than a patchy syntactic kludge. Should also reduce how much work needs to be done.

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 don't think so. There's nothing kludgy about this fix and it definitely reduces the amount of work more than what you suggest. When obtaining a contextual type we should never reach back down to the same type again unless we're absolutely sure that it won't again ask for a contextual type.

Copy link
Member

Choose a reason for hiding this comment

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

When obtaining a contextual type we should never reach back down to the same type again unless we're absolutely sure that it won't again ask for a contextual type.

We use resolveCall within getContextualType, which we know will call back into getContextualType - we just rely on the resolvedSignature circularity guards to hopefully prevent bad behavior. IMO, checking the expression in the context that we already have puts a hard ceiling on the contextual type (and is much more inline with that resolveCall philosophy of using existing resolution and circularity detection mechanics) and doesn't have the potential to change our behavior as much. I also think this change isn't going to affect our tests much one way or the other (especially if we're not adding a regression test) because we only have about two tests covering this branch of getApparentTypeOfContextualType, which were drastically simplified to only have literals as, at the time, that was all that was needed for the original repro. This branch of code is just for the case that a contextual type is discriminable based on what's already been defined, after all.

@ahejlsberg ahejlsberg merged commit a5029e3 into master May 22, 2018
@ahejlsberg ahejlsberg deleted the fixDiscriminatedContextualType branch May 22, 2018 17:35
@mhegazy mhegazy mentioned this pull request May 22, 2018
@microsoft microsoft locked and limited conversation to collaborators Jul 31, 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.

RangeError: Maximum call stack size exceeded with union type
4 participants