Reputation: 527
I was playing around with typescript and I noticed something unexpected.
I can see in showCarInfo2
function an error
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
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:
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)
}
}
Upvotes: 2
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)
}
}
Upvotes: 6