Skip to content

Narrowing inconsistencies #9260

Closed
Closed
@yortus

Description

@yortus

TypeScript Version:

nightly (1.9.0-dev.20160619-1.0)

Code

Three functions that narrow three types to see how narrowing behaves. foo1 and foo2 are identical apart from which type guards they use.

// A discriminated union with three cases
interface A { type: 'A' }
interface B { type: 'B' }
interface C { type: 'C' }

// Custom type guards for A/B/C
function isA(x: A|B|C): x is A { return x.type === 'A'; }
function isB(x: A|B|C): x is B { return x.type === 'B'; }
function isC(x: A|B|C): x is C { return x.type === 'C'; }

// Using if/else with type guard functions
function foo1(x: A|B|C): any {
    x // x is A | B | C
    if (isA(x)) {
        return x; // x is A
    }
    x // x is B | C
    if (isB(x)) {
        return x; // x is B
    }
    x // x is C
    if (isC(x)) {
        return x; // x is C
    }
    x // x is C, shouldn't x be 'never' here?           <===== (1)

    // If x was 'never', #8548 should apply here:
    if (isA(x)) {
        return x; // x is C & A                         <===== (2)
    }
    else {
        x // x is C                                     <===== (3)
    }
}

// Using if/else with discriminated unions
function foo2(x: A|B|C): any {
    x // x is A | B | C
    if (x.type === 'A') {
        return x; // x is A
    }
    x // x is B | C
    if (x.type === 'B') {
        return x; // x is B
    }
    x // x is C
    if (x.type === 'C') {
        return x; // x is C
    }
    x // x is A | B, shouldn't x be 'never' here?       <===== (4)

    // If x was 'never', #8548 should apply here:
    if (x.type === 'A') {
        return x; // x is A                             <===== (5)
    }
    else {
        x // x is B                                     <===== (6)
    }
}

// Using switch/case with discriminated unions
function foo3(x: A|B|C): any {
    x // x is A | B | C
    switch (x.type) {
        case 'A': return x; // x is A
        case 'B': return x; // x is B
        case 'C': return x; // x is C
        default:
            x // x is never                                 <===== (7)
            if (isA(x)) {
                x // x is never, why not A due to #8548?    <===== (8)
            }
    }
}

Expected behavior:

  • The inferred types at (1)/(2)/(3) should be the same as the inferred types at (4)/(5)/(6)
  • The inferred type at (1) and at (4) should be never, like it is at (7)
  • Type of x should be A at (8) due to Type guards as assertions #8548

Actual behavior:
See the comments in the code.


I wrote this code trying to understand what was going on in #9246 and #9254, and now I think I'm more confused. Firstly, foo1 and foo2 have identical logic, but tsc infers types differently in each. Secondly, I couldn't work out how to invoke the 'type guards as assertions' (#8548) behaviour. I thought it should kick in after (1) and (4), where the types have been exhausted and then another type guard appears. But control flow analysis never infers never in either function. We do get never inferred at (7), but then #8548 doesn't seem to apply at (8) where I expected it should.

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions