Back to all blogposts

New TypeScript features bring even more efficiency & protection to your code. Check out TypeScript version 4.1-4.7 overview

Wiktor Toporek

Wiktor Toporek

Senior Frontend Developer

TypeScript came around to enhance JavaScript with a number of features developers had longed for. It made JavaScript less error-prone and more scalable through strong typing. But the evolution of TypeScript is far from finished. Let’s take a look at the features added in versions 4.1-4.7 with special consideration for those that make a difference in real-life scenarios I encountered in my projects.

Do you like TypeScript? You probably already know that, but you’ve got company. A lot of company.

There are a lot of people who swear by TypeScript’s biggest advantages such as:

  • static typing, which greatly improved the predictability of the code,
  • improved IDE experience, as types make it easier for various tools (e.g. VS Code’s Intellisense, or TypeScript’s often control flow analysis offered by the type checker) to offer highly contextual intelligent code completion features,
  • easier debugging and better Developer Experience as a whole.

Just how many people are we talking about? Well…

TypeScript’s popularity

According to our State of Frontend 2022 report on frontend trends, as many as 84.1 percent of frontend developers used TypeScript in the last 12 months preceding their taking part in the survey.

TypeScript’s popularity

Marcin Gajda, Frontend Team Manager at The Software House, agrees that developers universally embraced TypeScript as the go-to way of writing JavaScript code and the trend will continue in the coming years. He believes that the main reason for that is the way TypeScript prevents a whole class of bugs before they can even happen, making development faster and more reliable. He adds that: 

After far too many years of web development feeling laborious and painful, frontend developers don’t want to re-live the experience of switching between the code editor and the browser back and forth multiple times to guess why “undefined is not a function.

Marcin Gajda

Marcin Gajda

Frontend Team Manager

Indeed, the research shows that as much as 15 percent of JavaScript bugs can be prevented by switching to TypeScript alone. The way TypeScript made it easier to orient oneself in a large complex database turned JavaScript into a more viable option for large commercial projects. It is something our TypeScript development team can definitely agree on.

But enough with that. If you made it this far, you probably know a thing or two about TypeScript already. Let’s get to the new features.

For starters, let’s take a look at how Microsoft chooses to handle TypeScript releases.

No offense – this type also has its uses!

TypeScript releases explained

The release cycle for previous TypeScript versions and the upcoming ones as organized by a team at Microsoft is quite predictable. Here is the gist of it:

  • There is a new release every 3 months with little to no variance in this regard.
  • Each new release gets a beta version, followed by a Release Candidate version 6 weeks after, followed by a stable release in the 2 weeks coming afterward.
  • Patches are less regular. The team at Microsoft reviews the need for patches every month and releases them as soon as they’re ready.
TypeScript release cycle

You can read more about the issue here.

TypeScript’s roadmap

With such an orderly release schedule, it is quite easy to predict the pace and nature of the language’s growth. A good way to go about this is checking the Roadmap, which includes the overview of features and fixes scheduled for a specific release.

As I am writing this article, the latest version we know anything about is TypeScript 4.9, scheduled for release in November 2022.

Microsoft’s DevBlogs is another great resource when you want to stay up-to-date and get all the details. It posts updates about all the new releases, including betas and Release Candidates.

TypeScript version 4 notable features

Ever since TypeScript 4.0 hit the shelves, the community saw the release of quite a few exciting features. I’m going to describe in detail the three that I believe to be the most relevant in the everyday work of developers, based on my experience as a frontend developer at The Software House.

Narrowing for variables destructured from Discriminated Unions (version 4.6)

TypeScript has a powerful type inference feature. You can often make some assumptions just by analyzing code branches. A nice real-world example of the superpower put into use is something that I tend to call: tailor-made union types.

Let’s use a React hook that is able to query an API for some user names:

I can write a React component that renders a correct message for the user depending on the current state of the query:

This is nice but there is a JavaScript feature that developers like to use when dealing with objects. It’s called object destructuring. I can use it to shorten the code above like this:

Although this would work in JavaScript without any problems, TypeScript 4.5 would have rested that code on the grounds that it is not type-safe. It would not have been convinced that the data variable in the SUCCESS branch is an array. 

The reason for that is that in TypeScript 4.5, destructured variables lost their context.

As a result, TypeScript has no way of knowing that successful status guarantees that there is an object that represents a successful query state.

Luckily, this has been improved in TypeScript 4.6 and the above code compiles without any problems. It seems to me that this improvement will also make developers less tempted to use some TypeScript escape hatches such as the bang operator or type-casting.

Read more about tailored-made union types in TypeScript in another article of mine.

Optional variance annotations (version 4.7)

To understand this feature, there are three concepts you should know a little about:

  • covariance,
  • contravariance,
  • invariance

There is a lot to them. If you want an in-depth explanation of them, you can definitely find the information you desire on the internet. Today, I’m going to give you just a very simplified TLDR version required to proceed with the article. 

Let’s say that your app has classes called User and Admin:

As you can see, all admins are users but not all users are admins. In this example, Admin is a subtype of User. How does this work when I confront these classes with generic typing?

Let’s say that I want to list some users from the database:

When I expect to get users from the getter, it does not matter if they are admins or not. In this context, it’s okay to forget that admins are users. For instance, if I want an email, it does not matter if I get Admin or User because both have the email property.

However, the problem arises when I expect to get a list of admins, but all I can do is get a list of users (some of which are not admins). That does not comply because some specific piece of code that makes use of that listAdmins function may want to check if an admin is a super admin or not. If that function cannot guarantee that a given user is an admin, it does not fit on the type level.

The above example shows how the type T behaves when it is covariant. In short words, the covariance of a type T in a generic type G<T> means that G<T> can be subtyped the same way as T.

Are you able to find a counter-example for this? Let’s try this with function arguments this time:

This time, the result is different. 

It is alright to replace an admin consumer with a user consumer. That’s because if a given function successfully consumes users, it can do so with admins by extension since the admin is just a specific type of user. However, a user consumer can’t be replaced with an admin consumer. The latter is specialized in working with admins. Some users may not have the required isSuperAdmin property that may be required here.

This is also an example of a generic type that has a contravariant type. Contravariance happens when the subtyping direction gets flipped in the generic type context. For instance, Admin is a subtype of User, but in the CollectionConsumer context, it’s the opposite since CollectionConsumer<User> is a subtype of CollectionConsumer<Admin>.

TypeScript 4.7 introduces optional variance annotations into the generic type definition syntax.

For instance, when you want to explicitly tell the TypeScript compiler that the generic type parameter should be covariant, you add the out prefix in front of the type. Consider the following example:

If you want to explicitly define the contravariant type, you can use the in prefix:

In and out are nicely selected keywords, because rather than the academic terms such as covariance or contravariance, they represent where the type parameter is actually placed. The location can be either in the input of the function or on the output.

You may ask: Why should I care about that?. TypeScript is really good at type inference already and it can spot a lot of type variance problems without any hints.

But, there are at least two reasons why this new syntax can be useful:

  1. When types get really complex (e.g. recursive ones). TypeScript’s compiler may lose track and infer the type parameter incorrectly. The hint can help prevent that.
  2. Because performance matters. Just good type inference in TypeScript is good, it doesn’t mean that it is fast. You’ll be able to boost type-checking by using in or out keywords. When the compiler sees them, it can determine type correctness in variance terms faster.

This may be promising for generically-typed libraries. It may boost type checking in some data validation or form libraries too. After all, in libraries like these, you expect to work on data structures that you define yourself.

There is one more case worth considering: can you mix covariance and contravariance together. Let’s find out what happens when you use both in and out keywords together:

It works, the type parameter can be used in more than one place in a generic type. What’s more, it can be both covariant and contravariant at the same time. However, to meet both criteria, the type can’t evolve either into a subtype or supertype. It has to be an exact match. A type like this is called invariant.

An invariant type hint can be very useful. A good example is when you want to know the exact structure of your form before sending it to an API.

Consider an interface like this:

Implementation itself is not as interesting as the subject of our analysis. Let’s assume that there are already some form instances on the type level:

The difference between them is that during the registration of a user, both his first and last name are required. The user can also enter an affiliation code of another user that invited them:

But during the update, only the first or last name can be updated. At the same time, only the defined fields are to be updated.

It seems that on a type-level, the registration form can be reused for the updates:

This theoretical API would have a problem with affiliation code during updates as extra unknown fields often cause 400 errors in such cases. That’s because RegistrationMutationParameters works as a subtype of ProfileUpdateMutationParameters.

Luckily, TypeScript 4.7 provides optional variance annotations you can use to make sure that your form data payload will be an exact match. The in out prefix is all you need to do that:

You can now trigger an error in case the exact match does not take place:

Error trigger

Template Literal Types (version 4.1)

This feature was an instant hit the minute developers got wind of it. And yet, the opportunities it creates continue to amaze me. But first things first. 

When working with JavaScript files, there is a feature called Template Literals. It enables you to interpolate dynamic values inside predefined strings:

From TypeScript 4.1, you can use analogous syntax on a type-level:

This can become very handy when used together with generic types. Let’s assume you have a function capable of adding a prefix to all property keys in a given object:

The problem with the type signature of a function is that you lose information about the types of original properties:

However, thanks to the synergy of generic types, mapped types, and template literal types features, it’s possible to fix that problem:

There are also Intrinsic String Manipulation Types that enable you to perform string operations such as conversion to lower or upper case:

However, it truly is at its most powerful when combined together with conditional types and their infer keyword. That’s because it enables you to parse strings on a type-level. This for instance enables you to convert property naming conventions such as snake_case into camelCase on a type-level. What’s most exciting is that it enables people to parse almost anything, even languages such as CSS or SQL. Just take a look at this experiment with CSS selectors.

Here is a list of awesome things that people implement thanks to that feature. I highly recommend you give it a try. The more examples you see, the easier it is to understand just how practical it is.

Just how important is TypeScript for The Software House? You can find out about this and more in our Technology Radar, which categorizes technologies by how common they are in our projects. For all intents and purposes, it is a guide to The Software House’s tech stack.

 

TypeScript 4 versions 4.1 – 4.7 – remaining features overview

Of course, there are a lot of other features in TypeScript version 4.1-4.7 that haven’t been described in detail in this article. Many concern issues such as performance, improvements, or the JavaScript/Node.js ecosystem itself

Since I am interested primarily in TypeScript as a programming language, I omitted them here. The aforementioned DevBlogs resource is a great way to review all the features. Still, it’s worth it to quickly list other important changes in TypeScript.

Other TypeScript 4.1-4.7 features to pay attention to

FeatureWhy care?
Key remappingEnables straightforward manipulation of object keys by providing the “as” keyword, doing away with various workarounds and easy tricks.
Improved type alias preservationThanks to this out-of-the-box improvement, type errors can get more readable for complex and longish union types that contain references to defined types.
Separate write type propertiesProperty getters and setters no longer have to be the same type. It creates an opportunity for accepting various types that can represent data such as number | string | Date while preserving a getter, narrowed down to just a Date.
Symbol and Template String Pattern Index SignaturesBesides enabling [index: symbol]: any syntax, you can use indexing by means of a string pattern such as [optName: `data-${string}`]: any.
The Awaited Type and Promise ImprovementsNails the problem with types such as Promise<Promise<…>>
Supporting lib from node_modulesTypeScript version upgrades may become less painful in case of breaking changes in the built-in library typings, such as DOM APIs. In other words, you can use the explicit lib version you’ve installed through npm.
Control Flow Analysis improvementsThis feature may save you from superfluous type-casts in case of if statements that already determine a possible type in the specific code branch.
ECMAScript Module Support in Node.jsSince Node.js finally got support for ESM (ECMAScript modules), TypeScript had to catch up and introduce some of the features that make this new Node.js feature more usable from a TypeScript perspective.
Controle over module detection

TypeScript trends – what does the future hold?

What do all the changes mean for the present and future of TypeScript?

The type definition challenge

Can type definitions remain powerful, while also avoiding giving devs a steep learning curve? This is a question that the team at Microsoft needs to answer.

TypeScript builds on JavaScript and the latter is a very dynamic language. It is not an easy task to provide strong type safety without compromising on the flexibility that JavaScript offers. Still, it seems that TypeScript maintainers improve JavaScript type safety more and more with each new TypeScript release.

Having said that, there are developers that don’t like spending too much time on type definitions. They are not likely to benefit much from all of these fancy language features and improvements directly in their work. 

After all, our main task as developers in commercial projects is just to make runtime code fulfill business acceptance criteria. 

The good news for them is that TypeScript’s longish and hard-to-understand type definition issues should no longer be such a problem. TypeScript’s out-of-the-box error messages are clearly getting better. That in turn makes it easier for the authors of third-party libraries to include more helpful error messages.

Developer Experience will be the priority

The type issue clearly shows that one of TypeScript’s original selling points was improving the experience of coding in JavaScript. It will continue to be on the agenda as TypeScript devs try to balance sophistication with simplicity. 

For now, they seem to be succeeding. For example, a junior developer may not be required to know how to use Template Literal Types magic, but they can still benefit from this feature indirectly by using a library such as kysely. Thanks to the Typesfeatures, the library can suggest exact table columns in SQL queries to those less experienced devs:

Demo of the kysely result type inference based on SQL query that dev-user provides

Different strokes for different folks?

It is always exciting to see what advanced TypeScript users are able to achieve when a new language feature shows up in the latest release. 

It seems to me that as TypeScript becomes bigger and bigger, it will increasingly have the capacity to appeal to different user groups for different reasons.

  • Juniors will use it because it’s popular and that’s what everyone is doing.
  • Static-typing seniors will seize the opportunity to protect juniors from having runtime errors.

With that said, what kind of new features can you expect in the future?

TSH Academy is our internal program in which experienced developers pass their experience and knowledge onto juniors. TypeScript is one of our mentors’ favorite technologies!

New TypeScript features I’m waiting for

There are some things I would like to see in TypeScript. Some of them are more likely to happen, some less so.

Static error analysis

Due to TypeScript’s stated goal of bringing static typing to the JavaScript world in a non-invasive way, TypeScript introduces optional static typing. As a result, you always have a couple of escape hatches where you can write unrestricted JavaScript code. 

However, it seems to me that TypeScript can still close some wide gaps for runtime error problems while keeping types optional.

I wish that TypeScript introduced sound static error typing so that a dev would know what error they can expect in that Promise.catch function callback. But that’s very unlikely to happen due to some technical limitations. There are also some custom workarounds for this, which a lot of developers know.

Non-structural types

In addition to improving static analysis of existing JavaScript code, TypeScript can add some value to it on a type level. It is worth remembering that even though TypeScript began as a structural type system, there is still an ongoing 8-year debate about extending it to support non-structural types. 

What excites me is that this debate finally got into the future roadmap last year and receives updates to this day. Who knows, perhaps this is going to be the next big thing in TypeScript?

TypeScript and non-structural types? Anything’s possible!

New TypeScript features – key takeaways

It amazes me how TypeScript evolved over the years. It started as a modest static analysis helper with the goal to make the overly flexible JavaScript world a bit more predictable. 

Then, it became a tool that detects type pitfalls in an increasingly smart and seemingly automatic way. 

Finally, it matured into a solution that helps design better APIs for things such as developers’ favorite ready-to-use libraries.

And it continues to evolve.

My overview of the latest TypeScript features leaves me with a couple of thoughts:

  • Improvements in discriminated unions are getting us closer to substantially improving a developer’s life without too big an impact on preferences of code structure.
  • Type inference is getting really powerful, especially complete with other features such as type guard , type string, optional chaining, or recursive type aliases.
  • But at the same time, exciting advanced features pop out all of a sudden. Some of them may seem almost academic. But then, a library author comes up with an experimental project that makes use of that Template Literal Type in an unexpected manner, making it available to a larger crowd.

In short, there is a lot of fun to be had and expected from all of you TypeScripts fans out there!

You may also like

What would you like to do?

    This site is protected by reCAPTCHA and the Google
    Privacy Policy and Terms of Service apply.

    We regard the TSH team as co-founders in our business. The entire team from The Software House has invested an incredible amount of time to truly understand our business, our users and their needs.

    Eyass Shakrah

    Co-Founder of Pet Media Group

    Thanks

    Thank you for your inquiry!

    We'll be back to you shortly to discuss your needs in more detail.