TypeScript 5.4 shipped on March 6, 2024, and in hindsight it fits the category of releases marketed as “incremental” whose real day-to-day impact is disproportionate. It does not bring a paradigm shift comparable to satisfies or template literal types, but it does retire three or four pain points that had been living in every moderately generic TypeScript codebase for years. The honest read is that there are two or three features you will use this very week, a performance improvement only visible in large monorepos, and a handful of additions that matter if you author libraries but are irrelevant if you only consume third-party types.
NoInfer: The Fix We Had Been Asking For
The headline addition is the NoInfer<T> utility type. It solves a specific but ubiquitous problem: when a generic function takes multiple parameters mentioning the same type parameter, TypeScript tries to infer it from all of them, widening the resulting type beyond what the author intended.
The canonical case is a function that takes a list of allowed values and a default value. If the second parameter participates in inference, you can pass a literal outside the intended set and TypeScript will accept it happily, because the act of passing it widens C enough to include it. Before 5.4 the fix was either two separate type parameters or a conditional type forcing distribution — a workaround only readable to someone who had already seen the pattern.
function createStreetLight<C extends string>(
colors: C[],
defaultColor?: NoInfer<C>
) {}
createStreetLight(["red", "yellow", "green"], "blue");
// Error: '"blue"' is not assignable to '"red" | "yellow" | "green"'
function withDefault<T>(value: T, fallback: NoInfer<T>): T {
return value ?? fallback;
}
withDefault(42, 0); // T = number, ok
withDefault("hi", 42); // Error — T pinned to string
The places where NoInfer pays off clearly are callbacks receiving already-inferred values, default-value parameters, options bags where the main type is deduced from another argument, and fluent builders whose shape is set by the first method call. The authors of Zod, tRPC, and React Query are adopting it in their upcoming major versions, so it reaches any modern project indirectly even if you never type the word yourself.
Personal style rule: use it when one parameter is clearly the source of truth and the others only consume that type. Skip it when parameters are genuinely symmetric or when a local cast reads just as well. It is not a badge for utility-type mastery.
Narrowing Preserved in Synchronous Closures
The second change you will notice this week is narrowing preserved inside callbacks executed synchronously. Historically, arr.every(x => typeof x === 'string') narrowed arr outside the if, but the moment you entered a forEach, map, or any array method, TypeScript forgot the narrowing because the callback could, in theory, be called later with arr mutated. The compiler now recognises that the standard array methods run synchronously and keeps the narrowing alive inside the callback.
The practical result is fewer as string[] scattered around, fewer defensive if (typeof x === 'string') blocks that added nothing, and fewer intermediate variables created solely to capture the narrowed type. It is one of those invisible improvements you only appreciate when you stop writing them.
Object.groupBy and Map.groupBy
TS 5.4 types the Object.groupBy and Map.groupBy methods that were at TC39 stage 3 at release time. The signature returns Partial<Record<K, T[]>>, with the Partial correctly marking that not every key in the union will appear. It is a direct convenience that removes a lodash dependency in projects that only used it for this. Not revolutionary, but it cuts one more reason to reach for a library.
Compiler Performance
The release notes mention lower memory use, faster incremental builds, and better type-instance deduplication. The truth is that in a ten-file project you will notice nothing. In a monorepo with hundreds of packages and a tsc --build that took minutes, the difference is perceptible — not transformative, but enough to make CI breathe. If your main pain point is type-check time, this release does not fix it; look at ts-go or parallel build setups.
What It Does Not Bring and What Can Break
Breaking changes are minor and localised: stricter homomorphism in mapped type variations may catch libraries doing aggressive type gymnastics, and some Function.prototype.bind signatures tighten. In application code the probability of noticing is essentially zero.
The absences are the familiar ones: full stable decorators, pipeline operator, pattern matching, branded types as a language construct. They all wait on prior moves in TC39 or designs that still lack consensus. Asking TypeScript for them is asking the wrong part of the chain.
Upgrading from 5.3
For the overwhelming majority of projects, migration is npm install -D typescript@5.4, running tsc --noEmit, and checking for new errors. If you use @typescript-eslint, bump the parser to a compatible version in the same commit to avoid a broken CI. Vite, esbuild, swc, Next, Nuxt, SvelteKit, and the rest of the modern stack need no changes. It is one of the least traumatic releases in years.
Conclusion
TypeScript 5.4 does not change how you think about types, but it retires three workarounds you had been writing for years without quite noticing. NoInfer turns into a trivial annotation what used to require two type parameters or an obscure conditional type, and the win propagates through the libraries everyone depends on. Closure narrowing eliminates an entire category of annoying casts. Object.groupBy closes the last excuse for pulling in lodash just to group a list. Performance helps where it used to hurt. The features still missing remain missing, but that is a TC39 problem, not a TypeScript-team problem. For an active team, upgrading early pays off: the new patterns make code more expressive, and the old ones start smelling like legacy faster than expected.