portfolio Anshul Bisen
ask my work

TypeScript 5.5 inferred type predicates changed how I write validation code

TypeScript 5.5 shipped inferred type predicates and it quietly eliminated an entire category of boilerplate in our codebase.

TypeScript 5.5 dropped on June 20th and the feature that got all the attention was the new config option for isolated declarations. Fair enough, that matters for library authors. But the change that immediately impacted our codebase was quieter: inferred type predicates. It sounds academic until you see what it does to real validation code.

For three years, every type guard function in our codebase required an explicit return type annotation. You had to tell TypeScript “this function is a type predicate” by writing the is keyword in the return type. If you forgot, the narrowing did not work and you got type errors downstream. With 5.5, TypeScript infers the predicate automatically from the function body. The annotation is optional. And the downstream impact on our payment validation pipeline was dramatic.

The shape of the problem before the fix.

In that first stretch at FinanceOps, I was still learning how to wear the Head of Engineering title without hiding behind it. It also builds on what I learned earlier in “How I use PostgreSQL as a job queue and why you probably should too.” The only credibility that mattered was whether the decision survived contact with real money, ugly edge cases, and the next person I would eventually hire. That same bias toward strict boundaries later shaped how I approached ftryos and pipeline-sdk: make correctness boring before you make the API clever.

The operational artifact behind the argument.

The Before and After

Our payment processing pipeline validates incoming webhook payloads through a chain of guard functions. Each guard checks one aspect of the payload and narrows the type for the next step. Before TypeScript 5.5, every single one of these guards needed an explicit type predicate.

// BEFORE TypeScript 5.5: explicit predicate required
function isValidPayment(
data: unknown
): data is PaymentWebhook {
return (
typeof data === 'object' &&
data !== null &&
'amount' in data &&
'currency' in data &&
typeof (data as any).amount === 'number'
);
}
// AFTER TypeScript 5.5: predicate is inferred
function isValidPayment(data: unknown) {
return (
typeof data === 'object' &&
data !== null &&
'amount' in data &&
'currency' in data &&
typeof data.amount === 'number'
);
}
// TypeScript infers: (data: unknown) => data is PaymentWebhook

The explicit version is not just more verbose. It is a maintenance hazard. When the PaymentWebhook type changes, you have to update the type predicate annotation separately from the function body. If they drift apart, the function claims to narrow to a type that the runtime checks do not actually verify. TypeScript trusts you completely when you write an explicit type predicate. With inference, the predicate is derived from the actual checks.

The Real Impact: Array Filtering

The biggest improvement was not in standalone guard functions. It was in inline filter callbacks. Before 5.5, filtering an array of union types required either an explicit type predicate or a type assertion.

// BEFORE: filter loses type information
const payments: (Payment | null)[] = await fetchPayments();
const valid = payments.filter((p) => p !== null);
// TypeScript 5.4: valid is (Payment | null)[] -- filter did not narrow!
// BEFORE: workaround with explicit predicate
const valid = payments.filter(
(p): p is Payment => p !== null
);
// AFTER TypeScript 5.5: inference just works
const valid = payments.filter((p) => p !== null);
// TypeScript 5.5: valid is Payment[] -- inferred correctly!

This pattern appears everywhere in our codebase. Fetching a list of records where some might be null. Filtering webhook events by type. Extracting successful results from a batch of Promise.allSettled outcomes. Every single one of these required either an explicit predicate or a cast. Now they just work.

The Refactoring Afternoon

I spent a single afternoon refactoring our codebase to remove explicit type predicates that were now unnecessary. The numbers tell the story.

  • 40 type guard functions across our validation, webhook processing, and data transformation modules.
  • 34 of them had type predicates that TypeScript 5.5 could infer automatically. I removed the explicit annotations.
  • 6 kept their explicit predicates because they narrowed to types that the function body alone could not prove. These were guards that checked discriminated union tags where the inference engine could not trace the relationship.
  • 23 inline filter callbacks lost their type assertion workarounds.
  • Net code reduction: 89 lines of type annotations removed.

Eighty-nine lines does not sound like much. But those were eighty-nine lines of type annotations that could drift out of sync with runtime behavior. Every one of them was a potential source of false confidence where TypeScript said the type was safe but the runtime check disagreed. Removing them made the codebase more honest.

When Explicit Predicates Still Matter

Inferred type predicates do not replace explicit ones in every case. There are three patterns where I kept the explicit annotation.

  • Discriminated unions where the tag check is separated from the narrowing. If the guard checks event.type === “payment.created” but the narrowed type is PaymentCreatedEvent, the inference engine cannot always prove the relationship.
  • Guards that perform transformations. If the function validates and transforms data simultaneously, the return type predicate documents the contract more clearly than inference.
  • Public API contracts in shared libraries. Explicit type predicates serve as documentation for consumers who cannot read the function body.

For everything else, let TypeScript do the work. The inference is correct, the code is shorter, and the type safety is stronger because it is derived from actual runtime checks rather than developer assertions.

What changed once the system matured.

That was the pattern of my first months at FinanceOps: I did not have management scar tissue yet, so I earned trust by making technical decisions that stayed boring under pressure. The same bias toward strict defaults still shows up in portfolio, pipeline-sdk, and dotfiles today.

TypeScript 5.5 is the most impactful minor release since 4.9 introduced the satisfies operator. If you are still on 5.4, upgrade today. The type predicate inference alone will clean up your codebase.