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

Narrowing discriminated unions with user-defined type guards #11787

Closed
timruffles opened this issue Oct 21, 2016 · 7 comments
Closed

Narrowing discriminated unions with user-defined type guards #11787

timruffles opened this issue Oct 21, 2016 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@timruffles
Copy link

timruffles commented Oct 21, 2016

TypeScript Version: 2.0.3

Code

type Cheese = {
  type: 'brie';
  brieOnly: true,
} | {
   type: 'cheddar';
  cheddarOnly: true,
}


function narrows<Wide extends string, Narrow extends Wide>(w: Wide, n: Narrow): w is Narrow {
    return w === n;
}


function doSomethingWithCheese(cheeseSample: Cheese) {
    if (cheeseSample.type === 'cheddar') {
        cheeseSample.cheddarOnly // ok
    }

    if (narrows(cheeseSample.type, 'cheddar')) {
        const t: 'cheddar' = cheeseSample.type; // has narrowed 
        cheeseSample.cheddarOnly // not ok, didn't narrow discriminated union
    }
}

Expected behavior:

Discriminated unions would work with type-guards in this way.

Actual behavior:

It doesn't, and I have to write a load of stuff like this in unit-tests:

    if (cheese.type === 'cheddar') {
          assert(/* some assertions using cheddar specific properties */);
    } else {
           throw Error("wrong type of cheese");
        }

rather than doing it in one step with an assertion throwing version of narrows:

    if (assert.narrows(cheese.type, 'cheddar')) {
          assert(/* some assertions using cheddar specific properties */);
    }
@aluanhaddad
Copy link
Contributor

What is narrowsInUnion in your example? I cannot find it.

@masaeedu
Copy link
Contributor

@aluanhaddad I think that's just a typo for narrows.

@masaeedu
Copy link
Contributor

masaeedu commented Oct 25, 2016

@timruffles It doesn't seem like what you're proposing generalizes well. Your narrows invocation is oblivious to the type of cheeseSample, you're just passing in an arbitrary expression of type string that you establish for the remainder of the function to be a particular string literal.

What you want to do can be accomplished with the existing type system, it is just somewhat awkward:

type Cheese = {
    type: 'brie';
    brieOnly: true,
} |
{
    type: 'cheddar';
    cheddarOnly: true,
};

function narrows<Wide, Narrow extends Wide>(w: Wide, f: (w: Wide) => Narrow): w is Narrow {
    return f(w) !== undefined;
}

function doSomethingWithCheese(cheeseSample: Cheese) {
    if (cheeseSample.type === 'cheddar') {
        cheeseSample.cheddarOnly;
    }

    if (narrows(cheeseSample, c => c.type === 'cheddar' ? c : undefined)) {
        const t: 'cheddar' = cheeseSample.type;
        cheeseSample.cheddarOnly;
    }
}

Right now I'm relying on type inference to make this work, but the fact that you have to use undefined and return the actual instance instead of a boolean is awkward. Possibly with some more indirection for type inference and narrowing in the language, this could be expressed less hackishly.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@seansfkelley
Copy link

This is still an issue in 3.x. I had a slightly simpler variant that didn't have a type-parameterized type guard, but I think it's the same basic issue:

interface Foo {
    type: "foo";
    foo: string;
} 

interface Bar {
    type: "bar";
    bar: string;
}

type FooBar = Foo | Bar;

function isFooType(t: "foo" | "bar"): t is "foo" {
    return true;
}

const f: FooBar = null as any;

if (f.type === "foo") {
    f.foo; // works
}

if (isFooType(f.type)) {
    f.foo; // breaks
}

playground link

This is a bummer, cause it means that a user-defined type guard deep in the type hierarchy may get bubbled up with "wrapper" type guards for containing types in order to discriminate them:

function isFoo(foobar: Foo | Bar): foobar is Foo {
    return isFooType(foobar.type);
}

@timruffles
Copy link
Author

@masaeedu the narrows call is not attempting to narrow cheeseSample, it's narrowing its type field. I'm suggesting that the information we gain from that narrowing is used to infer the type of cheeseSample.

If you re-read my example you'll see it's the same narrowing & inference logic as is used in the primitive example .type === 'cheese I gave above it, just with user-defined type guards rather than ===.

@cnradich
Copy link

cnradich commented Jun 3, 2019

Also running into this issue as of 3.6.0-dev.20190603. Another simple example:

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Square | Rectangle;

Using a basic Shape discriminated union example.

Demonstrates expected discriminated union type inference:

const shape1: Shape = JSON.parse('{ "kind": "square", "size" : 10 }');

console.log(shape1.kind); // compiles, as expected
console.log(shape1.size); // compiler error, as expected
if (shape1.kind === "square") {
    console.log(shape1.size); // compiles, as expected
}

Demonstrates incorrect discriminated union type inference using type guard:

const isShape = (value: any): value is Shape => typeof value === 'object' && value !== null && typeof value.kind === 'string' /* && etc... */;

const shape2: unknown = JSON.parse('{ "kind": "square", "size" : 10 }');

if (isShape(shape2)) {
    console.log(shape2.kind); // compiles, as expected
    console.log(shape2.size); // compiler error, as expected
    if (shape2.kind === "square") {
        console.log(shape2.size); // compiler error, NOT expected
    }
}

playground

@RyanCavanaugh RyanCavanaugh added Duplicate An existing issue was already created and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 22, 2019
@RyanCavanaugh
Copy link
Member

The narrows definition is not something that can narrow the outer type, as discussed above

Alternately, duplicate #30557

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

No branches or pull requests

6 participants