1

I was playing around with typescript and I noticed something unexpected. I can see in showCarInfo2 function an error

enter image description here

Why can't I use function for non null assertions?

In the first function showCarInfo1 there is car.passengers !== null and everything works fine.

interface Car {
  name: string;
  passengers: string[] | null;
}

const car: Car = {
  name: 'Seat',
  passengers: ['Andrew', 'Kate'],
}

function showCarInfo1(car: Car) {
  if(car.passengers !== null) {
    console.log(`${car.name}${car.passengers.map(passenger => ` ,${passenger}` )}`)
  } else {
    console.log(car.name)
  }
}

showCarInfo1(car)

const hasPassengers = (car: Car) => car.passengers !== null;

function showCarInfo2(car: Car) {
  if(hasPassengers(car)) {
    console.log(`${car.name}${car.passengers.map(passenger => ` ,${passenger}` )}`)
  } else {
    console.log(car.name)
  }
}

3
  • In most cases, checks in TypeScript are non-transitive. If you do, for example x = foo !== null; if (x) { foo++} you'd still get an error. It's a common problem and a known issue. Walking through the transitive conditionals and inferring types through them is apparently very costly for the compiler. Commented Mar 4, 2021 at 14:32
  • @VLAZ Is there any good solution or should I always use assertions like ` car.passengers !== null` even if I have to do this in multiple places? Commented Mar 4, 2021 at 14:39
  • 2
    You can create a user defined type-guard. But you also need some supporting infrastructure around it. I'll try to post an answer. Commented Mar 4, 2021 at 14:41

2 Answers 2

6

The compiler does not perform control flow analysis across function boundaries. While it would be nice if the compiler could do so, it would be prohibitively expensive. In general the compiler would have to do the equivalent of emulating every possible way the program could be run before it could make conclusions about the types, and assuming you want programs to compile before the heat death of the universe, there will be limitations. For a good discussion about this, see microsoft/TypeScript#9998, "Trade-offs in Control Flow Analysis".

In the absence of this happening automatically, there is a technique you can use to tell the compiler that the intent of hasPassengers acts as a type guard on its argument. You can annotate it as a user-defined type guard function. The return type is a type predicate of the form arg is Type and it's a special subtype of boolean (so user-defined type guards only work for functions that return boolean):

interface NonNullCar extends Car {
  passengers: string[];
}

const hasPassengers = (car: Car): car is NonNullCar => car.passengers !== null;

If you make the above change, your code will compile as desired:

function showCarInfo2(car: Car) {
  if (hasPassengers(car)) {
    console.log(`${car.name}${car.passengers.map(passenger => ` ,${passenger}`)}`) // no error
  } else {
    console.log(car.name)
  }
}

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

2

You can create user-defined type guard that checks if an arbitrary property of an object is present. To do that, you need a supporting generic type:

type HasProperty<T extends object, K extends keyof T> = T & { [P in K]-?: Exclude<T[K], null | undefined> };

This is a generic type that takes anything, as well as a key K of it as generics and with the Exclude utility type and the -? mapped type modifier constructs a type that has the same keys except:

  • Always has K
  • K is never null
  • K is never undefined.

In essence

type CarWithPassangers = HasProperty<Car, "passengers">

is like defining

interface CarWithPassangers {
  name: string;
  passengers: string[]
}

With this type, you can create a generic type guard:

const hasProperty = <T extends object, K extends keyof T>(obj: T, prop: K) : obj is HasProperty<T, K>  =>
    prop in obj 
        && obj[prop] !== null
        && obj[prop] !== undefined;

And finally use it like this:

function showCarInfo(car: Car) {
  if(hasProperty(car, "passengers")) {
    console.log(`${car.name}${car.passengers.map(passenger => ` ,${passenger}` )}`)
  } else {
    console.log(car.name)
  }
}

Playground Link

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.