Return to blog

Upgrading Typescript in your project from 4 to 5

hero image for Upgrading Typescript in your project from 4 to 5

Sometimes when you join a NodeJS project and work with legacy code you may face problems with old versions of the packages and some incompatibilities between them. In many cases, they are on particular versions due to specific reasons like breaking changes which don’t allow to easily bump versions, in other cases it is because developers sadly just don’t care about being up to date. One package which definitely should be always up to date is Typescript. This package is used only for development purposes but helps to create maintainable, efficient and safe code. In my case I had a situation where the backend was a big monolith project with a very old version of Typescript and it had to be upgraded to the newest one. Main reason was to improve project security and introduce helpful features of newer versions. I faced many refactor problems where code was not compatible with the updated package. I've decided to bump them one by one to the newer minor version. So from 4.3.5 I had eight steps to reach 5.3.3. I will try to introduce problems and solutions to adapt code to be compatible with the target version. I will also shortly describe changes in every new minor version that I've updated and possible adaptations in the TS configuration file. I will focus only on problematic cases which can force you to do code adaptations, show possible solutions and list only the useful new features in any version bump due to the full changelog being available on Typescript homepage.

4.3.5 -> 4.4.4



First change of typescript library was a little problematic due to a new helpful feature which can greatly increase the project security. Let's talk about that.

Unknown types of errors

First step in the process of updating TS to 4.4.4 was enabling the useUnknownInCatchVariables flag in tsconfig file. This flag was the main change in that bump and we decided to enable that. In short it forces caught errors in try-catch statements as unknown and not as any by default in the project. useUnknownInCatchVariables example of usage

What does that change mean to developers? In short there is more complexity in handling errors in the catch section. If you simply log error and eventually push/throw it forward there are no adaptations needed. But if there is some logic or handling properties inside that block you need to ensure that you know directly what type of error it is.

Look at the examples:

Before setting the flag, our error is of type any any typed error example

After setting the flag its type is unknown unknown typed error example

And in first case if we want to access message property of the error we can do it without receiving compilation errors since the type is any No error when error is any typed

In the second case - after setting the flag, we will receive a compilation error. This is due to the fact that the type unknown doesn't allow us to access a property which we don’t know about. Error when error is unknown typed

What can we do in that situation? We need to ensure that we directly know what we can access in the error object. For example checking that our caught error is an instance of a generic Error object. Checking instance of error example

But for sure we won’t be able to handle all possible situations. Everything depends on our try statement. It can be: an API call, an object validation, a file access etc. So we need to consider all the cases. Below you see possible scenario when we expect generic Error, AxiosError, ZodError or unknown error. Checking type of error example

If we are not sure yet what error type to expect but want to make code our buildable and allow to fix it in steps in some urgent situations we can set error type as any directly so the code will behave as if the flag wasn't enabled for this part of the code. This is not recommended. force any typed error example

Other new features in version 4.4 worth mentioning

  • static blocks in classes This allows you to write a sequence of statements with their own scope that can access private fields within the containing class. That means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class’s internals.




Version 4.5 doesn't have a very long list of breaking changes and new features but has very nice adaptations which can allow you for example to decrease the size of the compiled project.

New features in version 4.5 worth to mention

  • type Modifiers on Import Names The import type statement is used to import only the type information from a module. It indicates that you are interested in using the types of exported values but not the actual runtime values. It will give us less bundle size and can cause less load times and overall performance growth because it explicitly signals to the bundler that we don't need imported values. Code is also more readable because we strictly show that we are interested in importing types and they are in one scope not mixed with other modules.

If we want the compiler to warn us about that and force importing types directly with that statement set preserveValueImports flag in your tsconfig.json file.

We will have: Importing types separately with type clause

Instead of: Importing types along with other modules without type clause

  • support for module es2022

  • template String Types as Discriminants In your code you can check values that have template string types, and also recognize template string types as discriminants. Example shows us that we can indicate the type of variable and then access its properties.

Discriminants in types

4.6.4 & 4.7.4 & 4.8.4



Those versions are bundled together because they did not provide us much problems with upgrading but provide some helpful changes for type inferring, type narrowing and type comparison. Read about details in changelog links.




Last ver.4 before the jump to ver.5. provided us updates connected to for ex. editor helpers like:

  • "Remove Unused Imports" and "Sort Imports" Commands
  • Go-to-Definition on return Keywords

New types comparison possibility:

  • Check for equality with NaN

New operators:

  • The satisfies operator - lets us validate that the type of an expression matches some type, without changing the resulting type of that expression
  • Unlisted property narrowing with in operator - give us a possibility to check in object if properties exist, for example when we’re getting a response from a server or reading a configuration file




New major version gave us in the first line Speed, Memory, and Package Size Optimizations and provided a lot of new features but also strong changes. In many cases some of you will face problems when bumping to higher versions that do not directly relate for your project but to the packages that you are using inside it which may be incompatible with newer versions. But let's leave those specific cases and focus on new features and general possible problems.

New features

  • Decorators - give us a possibility to decorate functions inside classes out of the box
  • const type parameters - will allow us to determine readonly inference of the variable in function body
  • Enums now are unions - by default all enum members became separated types
  • Support for export type * - allow us to use exported variables only as types in imported files
  • Case-Insensitive Import Sorting in Editors

Possible problems inside codebase

Some places inside code people used to set type object or initialization of the variable as and then setting its properties. In TS 5 there were some changes in handling that situation that were not directly mentioned in the changelog. Type object is the type of all non-primitive values - and with this type, we can’t access any properties of a value (read or write will give us an error). In previous versions even when we use in operator on that type property it stays as an object and we cannot access properties. In many cases people used casting unknown typed variables as objects. In TS 5 if we use in operator on object type for example to check for example property we extends its type to intersection object & Record<’example’, unknown> so I guess it’s better to use Records instead of object type in the code base when we define something universal on which we need to do some business logic. In such cases we won't get an error when trying to access property from an object that is not directly defined. Using records instead of object type

Another case would be when we want to access a property whose key is inside a variable. Such cases may cause security/detect-object-injection warning or even an error (depending on your linter configuration). In many cases developers say that this error is a false-positive. But is it really? When you define that variable by yourself - true. But when you get that value from outside (for example user input, headers etc.) an attacker could then provide something unexpected, like the name of a method on the prototype chain. In such cases we need to check that the property name defined in the variable exists in our object. If you want to keep your code clean and safe, a good idea will be creating a function to safely retrieve values from objects.  Secure function to get property from object

Another helpful mechanism to handle types in such cases is the use of keyof and typeof operators separately or together. For example we want to delete properties from an object base on list of properties in second object to return only those props which are present in both objects. Using keyof on type we can produce a string or numeric literal union of its keys. Using typeof we can determine the type of an object. So using keyof and typeof we can produce a list of keys of object to use for getting data from an object property defined in a variable. keyof typeof clause example




No big changes worth mentioning.




  • Couple of nice features but not many big changes that would require adaptations in the code.
  • Copying Array Methods - TypeScript 5.2 includes definitions for the methods added to ECMAScript in the "Change Array by Copy" proposal.
  • Easier Method Usage for Unions of Arrays - string[] | number[] is transformed into (string | number)[] (or Array<string | number>)
  • Comma Completions for Object Members
  • Inline Variable Refactoring




  • Couple of nice features but not many changes that would require adaptations in the code.
  • Import Attributes - import attributes is used to provide information about the expected format of a module to the runtime.
  • Narrowing On Comparisons to Booleans
  • Interactive Inlay Hints for Types

As a reliable software company we’re focused on delivering the best quality IT services. However, we’ve discovered that programming skills give us a very particular opportunity...

.eco profile for

Reach Us

65-392 Zielona Góra, Poland

Botaniczna 70

© 2015-2024 Codetain. All rights reserved.