Skip to content

Type Narrowing not being all it can be #17319

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

Closed
johnnyreilly opened this issue Jul 20, 2017 · 2 comments
Closed

Type Narrowing not being all it can be #17319

johnnyreilly opened this issue Jul 20, 2017 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@johnnyreilly
Copy link

johnnyreilly commented Jul 20, 2017

TypeScript Version: 2.4.1

Hey all, I stumbled upon an oddity around type narrowing. I've been meaning to report for ages but... well better late than never!

Code

interface OneThing {
    oneThing: {};
}

interface OtherThing {
    otherThing: {};
}

const returnOneThingOrAnother = () =>
    '5' === `${6}` // ignore this - what's produced is what matters
        ? { oneThing: {} } as OneThing
        : { otherThing: {} } as OtherThing;

const iHaveOneThingOrAnother = returnOneThingOrAnother();

if (iHaveOneThingOrAnother.oneThing) { 
    // iHaveOneThingOrAnother should be narrowed to OneThing
} else {
    // iHaveOneThingOrAnother should be narrowed to OtherThing
}

Expected behavior:
I would expect type narrowing to occur in the manner suggested in the comments.

Actual behavior:
Actually the expected type narrowing does not occur. Rather, the if (iHaveOneThingOrAnother.oneThing) { expression throws the following exception:

Property 'oneThing' does not exist on type 'OneThing | OtherThing'.
  Property 'oneThing' does not exist on type 'OtherThing'.

Why? Obviously I could solve this by introducing a user defined type guard but that seems like it ought not to be necessary.

Is there a reason the TypeScript compiler does not perform the type narrowing in the way I had hoped?

Possibly related to #12457 but not certain.

Much ❤️ for all your work BTW 🌻

@RyanCavanaugh
Copy link
Member

Good question. We consider this to be an unsafe operation because we can't guarantee something like this doesn't happen:

interface GotString {
  x: string;
}
interface GotNumber {
  y: number;
}
const fooled = { x: 100, y: 200 };
const gn: GotNumber = fooled;
const j: GotString | GotNumber = Math.random() > 0.5 ? gn : { x: 'ok' };
if (j.x) {
  // In theory: j: GotString
  j.x.substr(0); // Crash - x was a number. Bamboozled!
}

See also #14094 where it's proposed to add syntax to create these sorts of unions automatically.

@johnnyreilly
Copy link
Author

Thanks for the response @RyanCavanaugh - that makes sense.

I'm quite keen on exclusive unions so at present I'm landed with either introducing a type guard or casting back to any briefly - neither of which is attractive. Suffice to say I like the look of #14094!

Do feel free to close this issue if you think it's covered by #14094. Good to know the reason for this 👍

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jul 20, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
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