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

Incorrect logical-or-operator type inferred with optional values and an empty object #49710

Closed
qtiki opened this issue Jun 28, 2022 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@qtiki
Copy link

qtiki commented Jun 28, 2022

Bug Report

πŸ”Ž Search Terms

empty object, infer, or operator, invalid type

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about empty objects

⏯ Playground Link

Playground link with a very simple example

πŸ’» Code

function getFoo(): 'foo' | undefined {
    return 'foo';
}
// explicit typing works here
const explicit: 'foo' | {} = getFoo() || {};
// implicit typing leads to `{}`
const implicit = getFoo() || {};

πŸ™ Actual behavior

When I use the logical or-operator to assign a value from a list of values that can be undefined and use the empty object {} as the last parameter the type gets inferred to only be the empty object instead of all the optional values that preceded it.

πŸ™‚ Expected behavior

I would expect the other types to be preserved. If I explicitly type the variable so that it can contain the preceding types as well as an empty object the type doesn't get flattened to just {}. If that were the case then I would expect the flattening to happen with the or-operator as well.

The use case where I ran into this was when I am receiving a list of objects from multiple sources and wanted to pick the first non-null value. However once I then do if ('xxx' in yyy) type of narrowing the object can potentially be null which prompted me to add the last default value as {} so that I could do the in check without worrying about null values.

Obviously I can work around this by using explicit typing or adding some bogus property to the empty object to make the inferring work but I felt that this is not how it's supposed to work. Since this is such a simple scenario I'm guessing this has been reported before and is expected functionality but I was not able to find any issue regarding this so I decided to open this.

@fatcerberus
Copy link

This is due to subtype reduction wherein subtypeValue || supertypeValue gets coalesced to just the supertype, and is by design, for better or worse. See, for example #46449 and the other issues linked at the bottom of that one.

@MartinJohns
Copy link
Contributor

Additionally, when explicitly typing it also accepts {}: const explicit: {} = getFoo() || {};

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jun 28, 2022
@RyanCavanaugh
Copy link
Member

Likely some confusion on this point - a string is a valid { } , so there is no wrongness here, just unexpcted imprecision

@qtiki
Copy link
Author

qtiki commented Jun 28, 2022

This is due to subtype reduction

Ah I don't think I've run into this concept before. That makes sense, so basically this is pretty much the same as something like this:

interface A {
    a: string;
}

interface B extends A {
    b: string;
}

const getA = (): A | undefined => ({ a: 'a' });
const getB = (): B => ({ a: 'a', b: 'b' });

const explicit: A | B = getA() || getB();

// implicitly typed as `A`
const implicit = getA() || getB();

It's just that the empty object is a supertype for so many things. I think I read somewhere about the concept of "explicit" empty object type. Something like that would probably help here. I suppose I could easily use something like Record<string, never> as an explicitly "empty" object type. πŸ€”

@fatcerberus
Copy link

Ah I don't think I've run into this concept before. That makes sense, so basically this is pretty much the same as something like this:

Yep, it’s the same mechanism in play.

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants