TypeScript: Property in subset of branches of a union type should be accessible for checking against undefined and result in narrowing type of object

Update

I opened this suggestion before learning that in does the narrowing that I was looking for (thanks to fatcerberus).

Maybe this suggestion should become one for an updated error message or a quick fix that converts if (u.p !== undefined) to if ("p" in u) when u is a union and p is a property only available in a subset of union members.


Suggestion

🔍 Search Terms

undefined, subset property, union, narrowing, type predicate, undefined test

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

⭐ Suggestion

TLDR: Type predicates are too much ceremony when a simple check against undefined will do

Details: Currently, when you have a union of types such as ({ a: number } | { b: string }), TS prevents you from writing this natural code:

if (x.a !== undefined) { // <-- Compiler error here...
    x; // <-- ... but it should work so that compiler can see x as { a: number } here
}

It’s annoying when you have just told the compiler that a property might exist, that you can’t then test whether the property does exist. It’s also annoying that when you have told the compiler that a property only exists in one branch of a union, that testing the property doesn’t let the compiler figure out that the object is in that branch of the union.

TS provides workarounds, but none of them are great.

You can provide a type assertion to get access to the member, but that doesn’t allow the compiler to narrow the type of x.

Alternatively, you can use a type predicate to distinguish between parts of a union, but that means defining a separate function which interrupts your flow and still requires you to use unintuitive code.

Here’s the type predicate example from the docs:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

You want to find out if something is a fish, so you, counterintuitively, have to declare that it is a fish before you can find out whether it is a fish.

Unfortunately type predicates are not very reusable. For example, this predicate does not tell us that pet is a fish in all cases. Instead, it allows us to distinguish between a fish and a bird. If we need to detect a fish when we might have a fish, a bird, and a dog, we need to create a new function or adjust the type of the existing one.

It would be a big usability win not to have to define type predicates for situations like this.

My suggestion is that code using the following pattern should compile without error and narrow the object:

  1. In a conditional
  2. Operation is a property access on a union type
  3. Property name exists in at least one branch of the union
  4. Property value is tested equal/not-equal to undefined

Note that the optional chain operator also fulfills these conditions, it’s not just expressions of the form if (a.b === undefined).

This change would neither break existing code (such code currently does not compile) nor would it fail to detect probable errors.

There is an error that would no longer be detected: If the user intended to enter the name of a property available in all branches of the union, but typoed and instead provided the name of a property only valid in a subset of branches, they would previously get a compile error, but with the change, the property access and test would now compile. This is not one to be concerned about.

📃 Motivating Example

ANNOUNCEMENT: Type predicates are needed in fewer cases, and more of your code will just work like you expect.

Checking a property against undefined is now allowed for union types even if the property is not present in all members of the union and the compiler will use that check to narrow the type of the object down to the appropriate member(s) of the union.

The following code will now compile successfully exactly as written:

type Fish = { swimming: boolean; };
type Cat = { purring: boolean; }

declare const pet: Fish | Cat;

if (pet.swimming !== undefined) {
    pet.swimming = true; // It's a fish
} else {
    pet.purring = true; // It's a cat
}

Previously, you would have had to introduce a type predicate to decide whether pet was a cat or a fish. Now you can just write code that checks the distinguishing property directly and TS will do the work of determining what types are appropriate.

đŸ’» Use Cases

It’s really quite common to want to distinguish between members of a union type (you’re defining an overloaded function or consuming someone’s API for example) and many times you do it by checking for the presence of a property that is only available in one of (or a subset of) the union’s members. Currently, the obvious code to detect property presence fails the type check and TS produces an error, so you have to go through the faff of creating a type predicate that is only useful in this one situation. This change would fix that.

This current behavior is probably my least favorite thing about TypeScript.

About this issue

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

Most upvoted comments

No worries. I appreciate the discussion and agree about slippery slopes - I’m all for using the type system. The thing is that users have to be able to distinguish between types sometimes. Do you have a sound way of doing that?

I actually missed that in does the narrowing I’m after, so thanks much for pointing that out. I’ll use that instead.