Boasting unique advantages over JavaScript, TypeScript is a popular programming language among the developer community. It just got even better with the release of TypeScript 5.4, which brings a host of helpful improvements and fresh features to the table.
In this guide, we’ll explore everything new with TypeScript 5.4, including the much-discussed “NoInfer” utility type, which can drastically improve TypeScript’s utility in specific situations.
All the Changes in TypeScript 5.4
Here’s a quick list of all the new additions included with TypeScript 5.4:
- A new utility type named NoInfer
- Smarter narrowing in closures
- Declarations for Object.groupBy and Map.groupBy
- Improved support for require() calls
- Accurate import attribute checking
- A quick fix for adding new parameters
- Improved auto-import support
- Behavioral changes
That’s quite a lot to unpack. So, without further ado, let’s dig into the details of each change and see what they mean for TypeScript users.
The New Utility Type – NoInfer
We’ll kick off with arguably the biggest addition of TypeScript 5.4: the new utility type, known as NoInfer. Because it gives TypeScript the ability to prevent inferring type arguments from the data it has available, this utility type has a lot of potential value, albeit in very specific situations.
In general, TypeScript has historically been quite effective at inferring type arguments from the data it has. You can call a generic function, and TypeScript will often figure out the right type argument to infer.
The key word there is “often,” not always. The system isn’t perfect, and TypeScript sometimes makes mistakes, which can cause a lot of confusion and errors for the programmer to address.
NoInfer aims to fix this, letting you exclude certain types from inference, and preventing TypeScript from inferring an incorrect or invalid type.
An Example
Let’s look at a very basic example of how this works.
Say you have a generic function that simply relays the type of whichever value you pass into it, like this:
const returnWhatIPassedIn = <T>(value: T) => value;
const result = returnWhatIPassedIn("hi");”
Here, TypeScript will infer the “result” type to simply be “hi.”
However, if we include the new NoInfer utility type, we can change that outcome.
const returnWhatIPassedIn = <T>(value: NoInfer<T>) => value;
const result = returnWhatIPassedIn("hello");
Now, thanks to NoInfer, TypeScript won’t automatically infer anything about “value,” so it can’t give anything back for “result.” If we want to get a specific return type from this function, we have to explicitly provide it.
Why Is This Important?
From the simplistic example provided above, you might be left wondering why NoInfer is needed, or what kind of value it can provide.
Well, where this utility type really shines is in functions that TypeScript cannot easily infer. Imagine you have a function that has multiple possible candidates for type parameter inference, or multiple parts of the code that TypeScript might use to make its inference.
If you just leave the code as it is, you’ve got two possible outcomes. You either have to hope that TypeScript infers the desired parameter, or you have to do a tedious and awkward workaround, like adding a separate type parameter bounded by the existing one to control which parameter is inferred.
NoInfer makes the whole process simpler.
It lets you give a direct and clear signal to TypeScript to not make any automatic inferences, giving you total control over inference candidates. It’s ideal for those situations where you have multiple runtime parameters all calling back to the same type parameter, or when you have complicated functions with multiple inference candidates.
Improved Narrowing in Closures
The next big change in TypeScript 5.4 concerns the narrowing process. This is when TypeScript effectively works out an exact variable type. For example:
function uppercaseStrings(x: string | number) {
if (typeof x === "string") {
return x.toUpperCase();
}
}
In this example code, TypeScript can figure out that “x” is a “string.”
However, if we use an example of an expanded function, it’s a different story.
function getUrls(url: string | URL, names: string[]) {
if (typeof url === "string") {
url = new URL(url);
}
return names.map(name => {
url.searchParams.set("name", name)
return url.toString();
});
}
In an example like this one, TypeScript isn’t able to automatically assess that “url” is a genuine URL object, because it gets mutated elsewhere in the code. So, even though it seems obvious to the programmer how “url” should be treated, the system doesn’t react the same way.

This is just one example of how types aren’t always preserved in TypeScript beyond function closures. It causes a lot of hassle for programmers, and it can lead to tedious and time-consuming adjustments of the code to get everything right.
Well, the 5.4 update aims to resolve these kinds of issues, once and for all, making the narrowing process much smarter, especially in non-hoisted functions. Now, whenever a parameter or “let variable" is in use in a non-hoisted function, TypeScript will automatically look for an ending assign point. If it finds one, it’ll narrow not only within that function but beyond it.
So, if we repeated the example above in 5.4, with its new and improving narrowing, it would work as desired.
Object.groupBy and Map.groupBy Declarations
TypeScript’s 5.4 update also brings fresh declarations to align with JavaScript’s recently added static methods, Object.groupBy, and Map.groupBy.
For those unfamiliar with this function, to use Object.groupBy, you need iterables, along with some sort of function with a key built into it. The key basically explains how elements should be sorted, and then Object.groupBy follows the rules of the key to create an object that sorts elements into their appropriate group.
For Map.groupBy, it’s a similar story, but with the end result being a map.
These static methods have proven useful for JavaScript development, and the 5.4 update brings their utility to the TypeScript userbase.
Improved Support for require() Calls
Long-term TypeScript users will be familiar with the language’s “bundler” moduleResolution option.
It’s designed to imitate modern bundlers, working out the right file that import paths refer to. However, it has its problems.
One of the downsides of the bundler moduleResolution option is how it always has to be aligned with “--module esnext.”
Because of that, it’s just not possible to make use of the “import ... = require()” code, which can be a problem in certain situations, especially if you’re working with conditional exports.
Well, with 5.4, require() can finally be used in conjunction with bundler, simply by setting the module to a brand-new option, entitled preserve. By using the “--module preserve” and “--moduleResolution” bundlers together, programmers should find it easier to precisely carry out module lookups and identify file import pathways.
Accurate Import Attribute Checks
Next up is a relatively small change, but a useful one all the same. TypeScript 5.4 will now check all import-related attributes and assertions against the ImportAttributes type. As a result, runtimes should become more precise and reliable in describing import attributes.
Rapid Solution for New Parameters
Another small change is that TypeScript 5.4 adds a rapid solution for adding a fresh parameter to any function that is called with an excess of arguments.
Let’s say you want to thread a new parameter or argument through a bunch of existing functions. Before 5.4, that could be a really awkward process. But now, it’s a lot easier and faster with this quick fix.
Improved Auto-Import Support
Prior to 5.4, TypeScript’s auto-import feature didn’t consider paths in “imports.” That was a problem, as users had to spend their own time and energy manually defining paths through their tsconfig.json.
Now, in 5.4 and beyond, that problem has been fixed. The auto-import feature now fully supports subpath imports, which should save programmers a lot of time and hassle.
Behavioral Changes
Lastly, there are a series of behavioral changes introduced in 5.4, which users may or may not notice, depending on how they use the language and the kind of code they work with.
Improved Accuracy in Conditional Type Constraints
Take a look at the following code:
type IsArray<T> = T extends any[] ? true : false;
function foo<U extends object>(x: IsArray<U>) {
let first: true = x;
let second: false = x;
Before 5.4, the “let first: true = x;” line would have produced an error. But the “let second: false = x;” line would not. Now, the second variable declaration is no longer allowed and will also result in an error, as TypeScript tightens its type constraints across branches.
Minimizing Intersections Between Variables and Primitive Types
TypeScript 5.4 also brings a stricter reduction in intersections between variables and primitive types. The specifics of how this works in practice will depend on how the variables and the primitives overlap.

Stronger Checks on Template Strings
TypeScript is now more precise and detailed than ever when verifying if strings can be assigned to placeholders (inside a string-type template). This should benefit programmers in the long run, though it may trigger code breaks when using constructs like conditionals, which may take some getting used to.
Type-Only Import Conflict Errors
In previous iterations, TypeScript has allowed a piece of code like this, provided the import to “Thing” referred to a type.
import { Thing } from “/path/";
let Thing = ABC;
Now, however, this kind of code will lead to an error message along these lines:
“’Thing’ conflicts with local value, so must be declared with an import.”
This is to prevent situations where single-file compilers automatically assume the import can be dropped.
Additional Enum-Related Restrictions
TypeScript 5.4 also delivers a range of new restrictions related to enums. Specifically, enum values now need to be exactly the same in order to remain compatible when known. Additionally, enum members can no longer have the names “Infinity” or “-Infinity.” “NaN” has also been removed from the list of allowed names.
What Do These Changes Mean for TypeScript Users?
The changes introduced in TypeScript 5.4 will mean different things for different users. It all depends on how you use this language and what kind of projects you work on.
For some, the introduction of “NoInfer,” coupled with the other changes, will have a dramatic effect, helping them be more productive and streamlined in their coding. Others may not notice the effects all that much in their work.
In general, though, these changes are all designed to make TypeScript a more reliable, effective, and useful programming language, addressing a range of known issues that had been flagged by the community. As a result, no matter how you’re using it, you should find TypeScript 5.4 a superior option to 5.3 and prior versions.
What’s Next for TypeScript?
Like other programming languages, TypeScript is effectively in a constant state of evolution, with more updates and iterations planned for the months and years ahead. In fact, TypeScript 5.5 is already in the beta stage and scheduled for a June 2024 release.
Like 5.4, 5.5 will bring even more new changes.
We know, for instance, that deprecated options and behaviors like “charset,” “noImplicitUseStrict,” and “importsNotUsedAsValues” will all return hard error messages in 5.5, and 5.4 is the last version of TypeScript to accept these options without providing hard errors.
The 5.5 version is also set to bring inferred type predicates, regular expression syntax checking, and improved control flow narrowing, among other additions and changes.
In the meantime, TypeScript fans and users will want to get used to the new quirks and additions introduced in 5.4, like experimenting with the NoInfer utility type, along with the Map.groupBy and Object.groupBy declarations.



