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

Fix #2214. Support narrowing with typeof in switch condition. #21957

Merged
merged 8 commits into from
Sep 6, 2018

Conversation

jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Feb 15, 2018

Enable narrowing support for typeof switch conditions. Fixes #2214

Implement narrow-by-assertion in switch with typeof, intending to replicate the equivalent behaviour with if-then-else.

@mhegazy Is there any possibility of this making the 3.0 milestone?

Basic Example:

function assertNever(x: never) { return x; }

function switchTypeOf(x: number | string | { x: boolean }): boolean {
    switch (typeof x) {
        case 'number': return x > 0;
        case 'string': return x === "narrowed";
        case 'object': return x.x;
    }
    return assertNever(x);
}

Exhaustiveness Checking:

Switch exhaustiveness checking is extended to support typeof.

function exhaustiveSwitch(x: number | ((x: string) => number) | symbol): number {
    switch (typeof x) {
        case 'number': return x;
        case 'function': return x('narrowed')
        case 'symbol': return 0;
    }
    // No implicit return
}

Order sensitive narrowing:

Repeated cases do not get the same narrowing.

function redundantCases(x: number | string | { x: boolean }): boolean {
    switch (typeof x) {
        case 'number': return x > 0;
        case 'string': return x === "narrowed";
        case 'object': return x.x;
        case 'number':
        case 'object': return assertNever(x);
        case 'string': return assertNever(x);
    }
    return assertNever(x);
}

Comparison with if-then-else.

1. Constrained parameters are not narrowed to never in default / else

function constraintNarrowingSwitch<T extends number | string | boolean>(x: T): boolean {
    switch (typeof x) {
        case 'number': return x > 0;              // x : T & number
        case 'string': return x === "narrowed";   // x : T & string
        case 'boolean': return x;                 // x : (T & true) | (T & false)
    }
    return assertNever(x); // <-- Error
    /* Argument of type 'T' is not assignable to parameter of type 'never'.
       Type 'string | number | boolean' is not assignable to type 'never'.
       Type 'string' is not assignable to type 'never'. [2345]
    */
}

function constraintNarrowingIf<T extends number | string | boolean>(x: T): boolean {
    if (typeof x === 'number') {
        return x > 0;
    }
    if (typeof x === 'string') {
        return x === "narrowed";
    }
    if (typeof x === 'boolean') {
        return x;
    }
    return assertNever(x); // <-- Error
    /* Argument of type 'T' is not assignable to parameter of type 'never'.
       Type 'string | number | boolean' is not assignable to type 'never'.
       Type 'string' is not assignable to type 'never'. [2345]
    */
}

2. Unions of constrained generic parameters.

type L = (x: number) => number;
type R = { x: string }

function genericUnionSwitch<T extends L, U extends R>(x: T | U) {
    switch (typeof x) {
        case 'function': return x; // x: T extends L
        case 'object': return x;   // x: U extends R
    }
}

function genericUnionIf<T extends L, U extends R>(x: T | U) {
    if (typeof x === 'function') {
        return x;  // x: T extends L
    }
    if (typeof x === 'object') {
        return x   // x: U extends R
    }
}

Code Review

The code comments are primarily there for review purposes, they should probably be taken out afterwards. A few things to signpost for the review:

  • The function getSwitchClauseTypeOfWitnesses performs no caching. The choice for this was because the cache type is Type[], the types of the clause values. In the context of typeof the types of the clause values are less important, rather what they represent. There didn't seem to be much use for caching the types. I'd like to get feedback on this, I'm not sure my decision is correct.
  • The function isExhaustiveSwitchStatement is updated to handle switch statements with typeof conditions.
  • Can (and should?) narrowBySwitchOnTypeOf be moved outside of the scope of reference flow typing?

@jack-williams jack-williams changed the title Support typeof in switch condition #2214 Support narrowing with typeof in switch condition #2214 Feb 17, 2018
@jack-williams jack-williams changed the title Support narrowing with typeof in switch condition #2214 Fix #2214. Support narrowing with typeof in switch condition. Feb 18, 2018
@jack-williams
Copy link
Collaborator Author

ping @mhegazy @RyanCavanaugh

Sorry for the ping, just wondering if this got lost under a pile of other stuff.

I'm not expecting anyone to drop what they're doing and look at this; I just want to make sure that it's clear I'm still actively wanting to try and merge this.

Initial draft that works for union types

First draft of PR ready code with tests

Revert changed line for testing

Add exhaustiveness checking and move narrowByTypeOfWitnesses

Try caching mechanism

Comment out exhaustiveness checking to find perf regression

Re-enable exhaustiveness checking for typeof switches

Check if changes to narrowByTypeOfWitnesses fix perf alone.

Improve switch narrowing:

+ Take into account repeated clauses in the switch.
+ Handle unions of constrained type parameters.

Add more tests

Comments

Revert back to if-like behaviour

Remove redundant checks and simplify exhaustiveness checks

Change comment for narrowBySwitchOnTypeOf

Reduce implied type with getAssignmentReducedType

Remove any annotations
@mhegazy mhegazy requested a review from weswigham May 22, 2018 00:27
@mhegazy
Copy link
Contributor

mhegazy commented May 22, 2018

@weswigham can you please review this change

// }
//
// The implied type of the first clause number | string.
// The implied type of the second clause is string (but this doesn't get used).
Copy link
Member

Choose a reason for hiding this comment

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

Is this comment correct? It seems a little off. Also could use a test which validates fallthrough behaviors, ie,

let x: string | number | boolean | object;
switch (typeof x) {
    case 'number':
        assertNumber(x)
    case 'string':
        assertStringOrNumber(x)
        break;
    default:
        assertObject(x);
    case 'number':
    case 'boolean':
        assertBooleanOrObject(x);
        break;
 }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, the comment is wrong. The line for the second clause should read something like:

"The implied type of the second clause is never, but this does not get just because it includes a default case"

@jack-williams jack-williams force-pushed the typeof-in-switch branch 3 times, most recently from 2bbb3d9 to 39d7d1e Compare May 23, 2018 02:49
@jack-williams
Copy link
Collaborator Author

After making some changes the CI failed due to strictNullChecks that weren't enabled when I initially developed the fix.

The source of the problem was using undefined to mark the default case in the list of types tested against. E.g.

switch(typeof x) {
    case "number": ...
    case "string"...
    default: ...
}

is represented as the array ["number", "string", undefined]. I'm not sure it's possible to completely ignore the default case, as you need to know where it is to correctly fix the clauseStart and clauseEnd. However, I have tried to limit the used of undefined by removing it once all the necessary information is computed.

I'm not convinced it's the most elegant solution, and would be more than happy to readdress the issue based on any feedback.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 11, 2018

@weswigham can you take another look.

@abraham
Copy link

abraham commented Jul 18, 2018

I'm currently implementing a RemoteData pattern in TS and I think this would greatly help to simplify my fold method.

@jack-williams
Copy link
Collaborator Author

I'm looking to add some more tests to this. If anyone has some examples that would be great!

@jack-williams
Copy link
Collaborator Author

Pinging @RyanCavanaugh for visibility because I think mhegazy was the only one watching this and I believe they've left the TS team.

@RyanCavanaugh
Copy link
Member

@weswigham can you review the current iteration? Thanks

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 actually looked at this yesterday but forgot to submit my upthumb. 👍

@weswigham
Copy link
Member

@jack-williams @RyanCavanaugh even though there's no conflicts, a merge with master in the branch prior to us pulling it in would be good - a few months passing may mean someone's added a test this may affect.

@jack-williams
Copy link
Collaborator Author

@weswigham @RyanCavanaugh Thanks! Master merged. CI failed on node 6 because of missing jake, I'm not sure if this was caused by my changes. Just let me know if I need fix anything.

@RyanCavanaugh RyanCavanaugh merged commit 8f654f0 into microsoft:master Sep 6, 2018
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.

5 participants