Skip to content

Track non-null unknown types in control flow analysis #45575

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 2 commits into from
Sep 9, 2021
Merged

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Aug 25, 2021

With this PR we fix the following long standing issue:

declare function obj(x: object): void;

function foo(x: unknown) {
    if (typeof x === 'object' && x !== null) obj(x);  // Ok
    if (x !== null && typeof x === 'object') obj(x);  // Was error, now ok
}

Above, the order of the checks mattered because unknown is an "indivisible" non-union type and therefore a check for x !== null couldn't narrow x in the second if statement. We implemented a partial fix in #37507, but this only worked for truthy checks combined with typeof checks using the && operator in the same expression.

With this PR we remove the partial fix and instead introduce an internal nonNullUnknownType that is used in control flow analysis to represent an unknown type that is known to be non-null. The non-null unknown type results when the unknown type is subjected to a truthiness check or an x !== null check, and when the non-null unknown is subsequently subjected to a typeof x === 'object' check, we narrow to object instead of object | null. Because we rely on control flow analysis, this solution properly reflects the effects of non-null checks in separate expressions or statements:

function bar(x: unknown) {
    if (!x) {
        return;
    }
    if (typeof x === 'object') {
        obj(x);  // Was error, now ok
    }
}

Fixes #28131.

return includes & TypeFlags.Any ? includes & TypeFlags.IncludesWildcard ? wildcardType : anyType : unknownType;
return includes & TypeFlags.Any ?
includes & TypeFlags.IncludesWildcard ? wildcardType : anyType :
includes & TypeFlags.Null || containsType(typeSet, unknownType) ? unknownType : nonNullUnknownType;
Copy link
Member

Choose a reason for hiding this comment

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

What’s an example where this produces nonNullUnknownType? Would nonNullUnknownType itself have to be one of the union constituents?

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, it'll produce nonNullUnknownType when there are no regular unknownType instances in the set (which implies all are nonNullUnknownType instances) and the set contains no TypeFlags.Null types. Effectively, a non-null unknown type survives only if there are no regular unknown types and no null types.

@@ -23765,7 +23763,7 @@ namespace ts {
valueType.flags & TypeFlags.Null ?
assumeTrue ? TypeFacts.EQNull : TypeFacts.NENull :
assumeTrue ? TypeFacts.EQUndefined : TypeFacts.NEUndefined;
return getTypeWithFacts(type, facts);
return type.flags & TypeFlags.Unknown && facts & (TypeFacts.NENull | TypeFacts.NEUndefinedOrNull) ? nonNullUnknownType : getTypeWithFacts(type, facts);
Copy link
Member

Choose a reason for hiding this comment

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

Can this not just be handled by getTypeWithFacts?

Copy link
Contributor

@ExE-Boss ExE-Boss Aug 26, 2021

Choose a reason for hiding this comment

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

Well, getTypeWithFacts(…) is used in many more places, and nonNullUnknownType should never escape control flow analysis: https://github.com/microsoft/TypeScript/blob/7e231a2ebfce3692663fe2583a19a8cb0c59e457/src/compiler/checker.ts#L23118-L23119

So probably not?

Copy link
Member

Choose a reason for hiding this comment

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

@treybrisbane
Copy link

This reminded me of #29317! 😮

Does special-casing this mean the team is still undecided on Negated Types?

@treybrisbane
Copy link

In any case, thanks for this fix! I have this pop up every so often, and it'll be nice to see it finally solved. 😀

@@ -753,6 +753,7 @@ namespace ts {
const nonInferrableAnyType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.ContainsWideningType);
const intrinsicMarkerType = createIntrinsicType(TypeFlags.Any, "intrinsic");
const unknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
Copy link
Member

Choose a reason for hiding this comment

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

It's already pretty hard to understand what these are for. I'd encourage us to start documenting what the use-cases for each of these are. For example.

Suggested change
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");
/**
* An `unknown` type that is used purely for narrowing.
* Used to record that a `x !== null` check has occurred to specially handle `typeof x === "object"`.
*/
const nonNullUnknownType = createIntrinsicType(TypeFlags.Unknown, "unknown");

@ahejlsberg ahejlsberg merged commit 5186ee3 into main Sep 9, 2021
@ahejlsberg ahejlsberg deleted the fix28131 branch September 9, 2021 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

typeof discards previous non-null checks
6 participants