TypeScript: Type of `([].length > 1 && {})` can be false but Typescript cannot detect it

Bug Report

🔎 Search Terms

object type condition parenthesis

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ.

⏯ Playground Link

Playground link with relevant code

💻 Code

// This throws an error as expected because "Spread types may only be created from object types"
const invalid = { ...(false) }

// This does not throw even though the value in parenthesis would also be resolved to `false` which
// is a side effect of wrong type detection. This assignment is allowed because Typescript cannot detect
// that there is a possibility that the value inside the parenthesis would be `false`.  
const valid = { ...([].length > 1 && {}) }

const wrongTypeDetection = ([].length > 1 && {}) // type of wrongTypeDetection is {} instead of false | {} 

🙁 Actual behavior

Type of const wrongTypeDetection = ([].length > 1 && {}) is {}

🙂 Expected behavior

Type of const wrongTypeDetection = ([].length > 1 && {}) should be false | {}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 16 (5 by maintainers)

Most upvoted comments

Let’s say your partner sends you to the store with a shopping list. They say “Get me the things on this list if they have them, but don’t get anything not on the list”. You look at the list and the item is just “totato”. What should you do?

You could go to the store and come back with nothing. That seems like a bad move, since surely something was intended here, and the store never stocks totatos. You could say “Did you mean potato, or tomato?” before leaving. That seems like the right move.

Next week, you go to the store, and they send a carrier pigeon with the shopping list to meet you there (the cell network is down). The list again says “totato”. You come back with nothing, since you’re not supposed to buy anything not on the list. Did you mess up?

One theory of the situation is that you were wrong to reject the initial list. The instructions were explicit: you should have gone to the store, checked for totatos, and came back with nothing. Errors are not your problem, even if they render the instructions 100% nonsensical.

In another theory of the situation, you shouldn’t have agreed to go to the store at all the second time, since you plainly reject some shopping lists and were given no proof that the carrier pigeon would carry a valid list. Your partner might bristle at this - carrier pigeons are very effective at delivering shopping lists, and supplying an up-front proof that the list will not contain problematic items is very difficult.

The third theory is that it’s okay to reject some shopping lists with upfront errors, while still accepting future lists even though they can’t be checked ahead of time, even though this is not consistent. In fact, this is probably the behavior that’s most desired – identifying detectable early errors is valuable, and accepting late-bound data is valuable, and consistency between the two behaviors is a distant third in terms of importance in being a useful grocery-getter.

By claiming to accept any grocery list you might get in the future, but rejecting some lists you can read right away, you’ve created a “hole” in the logic of your behavior, since any invalid list can be “made valid” by sending it via pigeon, which is illogical - yet still useful. It’s a subtyping symmetry violation. These are usually problematic, but not always – sometimes if you know more about a situation, you can provide better feedback than if you know less about a situation. That’s not a problem! Inconsistency in the face of different situations is not inconsistency, it’s application of data.

TypeScript has the same view – that there are operations which are facially problematic in the presence of statically-determinable facts (object-spreading a value like true, for example, which doesn’t do anything), but also operations which might end up doing the same thing (object-spreading {}) which should be presumed OK, because they have safe and reasonable no-op behavior.

@RyanCavanaugh Thank you for the explanation but that’s an overly complicated example that raises more questions than answering them.

But before that, can you also explain why is this invalid in Typescript?

const x = { ...'a string which works fine in vanilla JS' } // Spread types may only be created from object types 

You can’t reassign a const. Change that to let and you’ll see the assignment succeeds.

false is assignable to {}.