TypeScript 5.5 cut a small daily annoyance out of filter code.
The feature was inferred type predicates. That name sounds like release-note furniture, but the practical change is simple: TypeScript got better at understanding that a filter callback removed undefined, null, or another excluded value.
Before 5.5, code like this often needed help:
const scores = studentIds
.map((id) => scoreByStudent.get(id))
.filter((score) => score !== undefined)
const average = scores.reduce((sum, score) => sum + score, 0) / scores.length
Humans can see that scores contains numbers after the filter. Older TypeScript versions often kept the type as (number | undefined)[], which meant the next line still had to defend against undefined.
TypeScript 5.5 understands this common pattern. The filtered array narrows to number[].
That is not flashy. It is exactly the kind of improvement that makes a typed codebase feel less fussy.
the old workaround taught bad habits
The old workaround was usually a helper:
function isDefined<T>(value: T | undefined): value is T {
return value !== undefined
}
const scores = studentIds
.map((id) => scoreByStudent.get(id))
.filter(isDefined)
That helper is fine. I have written it plenty of times.
The problem is that codebases accumulated a small museum of these helpers: isDefined, nonNullable, exists, isPresent, truthy, compact, and whatever somebody copied from Stack Overflow three years earlier. Some were correct. Some quietly removed valid values. Some were too broad. Some were type assertions wearing a function costume.
When the compiler understands the obvious local case, teams can delete a surprising amount of noise.
truthiness is still a trap
The useful limitation is that TypeScript cannot treat every truthiness filter as safe.
const scores = [0, 82, 91, undefined].filter((score) => !!score)
That removes undefined, but it also removes 0. If 0 is a valid score, the runtime behavior is wrong. TypeScript is right to be skeptical.
The better version says what it means:
const scores = [0, 82, 91, undefined].filter(
(score) => score !== undefined,
)
This is the kind of type-system behavior I like. It rewards precise code instead of rewarding clever code.
Truthiness still works well for object values where falsey ambiguity is not the issue:
const users = userIds
.map((id) => usersById.get(id))
.filter((user) => user)
Even there, I usually prefer the explicit comparison because it documents the shape of the missing value.
better narrowing means fewer assertions
Type assertions are useful when the programmer knows something the compiler cannot know. They get dangerous when they become the default way to end an argument.
const scores = studentIds
.map((id) => scoreByStudent.get(id))
.filter((score) => score !== undefined) as number[]
That assertion used to feel understandable. It also made the filter line less trustworthy. If the callback changed later, the assertion could keep lying.
With 5.5, the normal code can carry the normal type:
const scores = studentIds
.map((id) => scoreByStudent.get(id))
.filter((score) => score !== undefined)
That is the win. The type comes from the code instead of from an override.
explicit annotations still have a job
More precise inference can surprise code that expected a wider type.
const nums = [1, 2, 3, null].filter((value) => value !== null)
nums.push(null)
In newer TypeScript, nums is inferred as number[], so pushing null is an error. That is usually correct. If the code intentionally wants (number | null)[], say so:
const nums: (number | null)[] = [1, 2, 3, null].filter(
(value) => value !== null,
)
nums.push(null)
That annotation is not noise. It records intent.
The distinction matters. The compiler should infer the common case, and developers should annotate the unusual case. That is a healthy division of labor.
arrays are where small friction compounds
Array helpers are everywhere in TypeScript code.
The common path is usually some version of map, filter, transform, group, render. If the type checker loses track of the value halfway through, every downstream line gets noisier.
React code hits this constantly:
const visibleItems = items
.map((item) => item.details)
.filter((details) => details !== undefined)
return visibleItems.map((details) => (
<DetailCard key={details.id} details={details} />
))
Data loading code hits it too:
const validRows = rows
.map(parseRow)
.filter((row) => row.ok)
.map((row) => row.value)
The exact narrowing depends on the shape, but the general point holds: when TypeScript tracks the array pipeline correctly, the rest of the code gets calmer.
predicates should stay honest
Inferred predicates do not remove the need to write honest predicate functions.
If a predicate mutates state, depends on external context, or returns a boolean for reasons unrelated to the value’s type, I do not want to rely on clever narrowing.
const activeUsers = users.filter((user) => {
audit(user.id)
return user.deletedAt === undefined
})
That may work, but it mixes filtering and side effects. The type system cannot make that a good idea.
The best filter predicates are boring:
const activeUsers = users.filter((user) => user.deletedAt === undefined)
Small feature, same old discipline.
I would still keep named predicate helpers when the predicate is a domain rule instead of a null check.
const billableEvents = events.filter(isBillableUsageEvent)
That name can carry product meaning. The 5.5 improvement mostly removes helpers whose only job was explaining undefined to the compiler. It does not replace the value of naming a business rule.
this is what good language maintenance feels like
I like TypeScript changes that remove pressure from ordinary code.
Inferred type predicates do not change how I architect an application. They do make a lot of tiny array pipelines read the way they should have read already. They reduce helper clutter, reduce assertions, and make precise checks pay off.
That is a good kind of language improvement. It does not ask for a new style. It makes the existing style less annoying.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.