Every TypeScript codebase I've worked on has this line somewhere:
const names = users.map((user) => user.nickname).filter(Boolean) as string[];
The as string[] at the end is the tell. nickname is string | undefined, the filter removes the undefined ones, and the type stays (string | undefined)[] because filter has no idea what Boolean does. So someone casts, and the cast stays there for five years.
TypeScript 5.5 came out three weeks ago and most of those casts can go. Let's see what it does, and what the hand-written version was for in the first place.
What a type predicate is
A function that returns boolean tells the compiler nothing. A function that returns value is string tells it to narrow:
// src/utils/guards.ts
export function isDefined<T>(value: T | undefined | null): value is T {
return value !== undefined && value !== null;
}
const names = users.map((user) => user.nickname).filter(isDefined);
// string[], no cast
value is T is the type predicate. It's a return type, true means the parameter is a T, and filter has an overload that picks it up. Every project ends up with a guards.ts full of these: isDefined, isString, isUser, isFulfilled for Promise.allSettled.
What 5.5 infers
The new part is that you don't have to write the predicate for the simple ones. If a function takes one parameter, has no return type annotation, doesn't reassign the parameter, and returns a single expression that narrows it, TypeScript infers the is for you:
const names = users.map((user) => user.nickname).filter((nickname) => nickname !== undefined);
// string[] in 5.5, (string | undefined)[] in 5.4
const isString = (value: unknown) => typeof value === 'string';
// inferred: (value: unknown) => value is string
Hover the arrow function and the editor shows nickname is string. My isDefined from above could lose its annotation and keep working. I kept it, because a name is still nicer than an inline arrow, but the one-off filters all over the app got their casts deleted.
What it still doesn't
filter(Boolean). Boolean is declared in the lib with a plain boolean return type, and inference only looks at function bodies in your code. This one stays as it was. Write the arrow.
Truthiness on strings and numbers. (name: string | undefined) => !!name is not inferred as name is string, and it's right not to be: '' is a string and falsy, so the false branch would be lying. Use !== undefined when you mean undefined. For objects and arrays, which are always truthy, !!value is inferred fine.
Anything with a named type. (value: unknown): value is User still has to be written, because TypeScript can infer from the checks that something is an object with an id, not that you'd like to call it User. For data from outside, that's the job of a schema anyway, see the Zod post.
The predicate that lies
Here's the part I wish someone had told me in 2019. The body of a hand-written predicate is not checked against its promise:
// compiles without a complaint, and it's wrong
function isUser(value: unknown): value is User {
return typeof value === 'object';
}
TypeScript checks that User is assignable to unknown, which it is, and that the function returns a boolean, which it does. Whether the boolean is right is on you. isUser(null) returns true, and now null is a User three lines down with the compiler's blessing.
An inferred predicate comes from the body, so it can't disagree with it. That's what I like most about 5.5: fewer places where a human wrote the promise.
Assertions, the other kind
A cousin that throws instead of returning false:
// src/utils/assert.ts
export function assertDefined<T>(value: T, name: string): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(`${name} is missing`);
}
}
const root = document.getElementById('root');
assertDefined(root, 'root');
root.append(app); // HTMLElement from here, no `!`
asserts value is T narrows for the rest of the scope once the function returns. I use it for the three or four places where something has to be there and the only sane response to it missing is to crash early. Same warning: the body isn't checked, so keep these short and dumb.
Dan Vanderkam wrote the inference feature and a post about how it was made, which is a better read than this one.
Read more here:
- Announcing TypeScript 5.5 by Daniel Rosenwasser
- The Making of a TypeScript Feature: Inferring Type Predicates by Dan Vanderkam
- Using type predicates in the TypeScript handbook