Luke
Luke

Reputation: 527

Why can't use function to assert that var is not null in typescript

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)
  }
}

Upvotes: 1

Views: 859

Answers (2)

VLAZ
VLAZ

Reputation: 29086

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

Upvotes: 2

jcalz
jcalz

Reputation: 329308

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

Upvotes: 6

Related Questions