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

Suggestion: successive narrowing with nested type guards #9016

Closed
yortus opened this issue Jun 8, 2016 · 1 comment
Closed

Suggestion: successive narrowing with nested type guards #9016

yortus opened this issue Jun 8, 2016 · 1 comment
Labels
Duplicate An existing issue was already created

Comments

@yortus
Copy link
Contributor

yortus commented Jun 8, 2016

// Supported beast features
interface Beast     { wings?: boolean; legs?: number }
interface Legged    { legs: number; }
interface Winged    { wings: boolean; }

// Beast feature detection via user-defined type guards
function hasLegs(x: Beast): x is Legged { return x && typeof x.legs === 'number'; }
function hasWings(x: Beast): x is Winged { return x && !!x.wings; }

// Function to identify a given beast by detecting its features
function identifyBeast(beast: Beast) {

    // All beasts with legs
    if (hasLegs(beast)) {

        // All winged beasts with legs
        if (hasWings(beast)) {
            if (beast.legs === 4) { // ERROR TS2339: Property 'legs' does not exist on type 'Winged'.
                console.log(`pegasus - 4 legs, wings`);
            }
            else if (beast.legs === 2) { // ERROR TS2339...
                console.log(`bird - 2 legs, wings`);
            }
            else {
                console.log(`unknown - ${beast.legs} legs, wings`); // ERROR TS2339...
            }
        }

        // All non-winged beasts with legs
        else {
            console.log(`manbearpig - ${beast.legs} legs, no wings`);
        }
    }

    // All beasts without legs    
    else {
        if (hasWings(beast)) {
            console.log(`quetzalcoatl - no legs, wings`)
        }
        else {
            console.log(`snake - no legs, no wings`)
        }
    }
}

// Runtime results
identifyBeast({ wings: true });             // quetzalcoatl - no legs, wings
identifyBeast({ wings: false });            // snake - no legs, no wings
identifyBeast({ legs: 2 });                 // manbearpig - 2 legs, no wings
identifyBeast({ legs: 4 });                 // manbearpig - 4 legs, no wings
identifyBeast({ wings: true, legs: 2 });    // bird - 2 legs, wings
identifyBeast({ wings: true, legs: 4 });    // pegasus - 4 legs, wings
identifyBeast({ wings: true, legs: 6 });    // unknown - 6 legs, wings
identifyBeast({ wings: false, legs: 6 });   // manbearpig - 6 legs, no wings

The code above produces compiler errors with tsc@next, but it does represent valid JavaScript that runs correctly and outputs the expected results as shown at the bottom of the code.

I believe tsc is working as intended in this example. So this issue is a suggestion for improving type guards to support this coding pattern (sometimes called duck typing 🐥). The rationale is that it is a useful and common practice in JavaScript to identify and refine objects through successive feature detection.

The limitation with type guards in their current form is that if we have already narrowed to Legged, and then we further narrow to Winged using a nested type guard, then tsc 'forgets' that we still have a Legged. Imagine a blindfolded person feeling the legs of a beast. If further investigation reveals that the beast has wings, the person would probably assume the legs are still there. After all, legs and wings are not mutually exclusive features, and a beast can have both.

So the ideal behaviour in this case, would be that if we narrow to Legged and then further narrow to Winged, then its safe to say we must now have a Legged & Winged, ie both features simultaneously. (Strong duck typing? 💪 🐥)

A similar issue can be seen in the following snippet:

function beastFoo(beast: Beast) {
    if (hasWings(beast) && hasLegs(beast)) {
        beast // beast is Legged
        // ideally, beast would be Winged && Legged here...
    }

    if (hasLegs(beast) && hasWings(beast)) {
        beast // beast is Winged
        // ideally, beast would be Legged && Winged here...
    }
}

In summary, in a block of code that is only reachable by passing multiple type guards (eg isFoo(x) && isBar(x) && isBaz(x)), the narrowed type inside that block would be the intersection of the narrowed types from the type guards (eg x is Foo&Bar&Baz).

@yortus yortus changed the title Suggestion: type guards for feature detection Suggestion: successive narrowing with nested type guards Jun 8, 2016
@ahejlsberg ahejlsberg added Suggestion An idea for TypeScript Duplicate An existing issue was already created and removed Suggestion An idea for TypeScript labels Jun 8, 2016
@ahejlsberg
Copy link
Member

This looks like a duplicate of #8911. We labelled #8911 as "Working as Intended" but I will change it to "Suggestion".

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants