Mentor: “TypeScript is a virus!”

Max Kayander
JavaScript in Plain English
6 min readMay 26, 2023

--

Kyle Simpson is widely recognized as one of the most known, respected, and admired engineers in the industry. His books on JavaScript have garnered a significant following, with many of us, myself included, owning and having read them.

Recently, I came across a post where he shares his insights on the current state of TypeScript. In his writing, he suggests that TypeScript is rapidly gaining dominance in codebases, even though he expresses concerns about its perceived lack of necessity and potential negative impact due to its complicated syntax compared to plain JavaScript.

In one of the comments below the post, he says:

“Holy hell, TS is a virus that infects just about every codebase I have run across…”

I want to make it clear that I am not questioning Kyle’s competence in any way. Rather, I simply want to share my thoughts on this perspective.

Is TypeScript a trap we all fell into? Let’s dive in!

Bad TypeScript code

TypeScript is a tool. Obviously, it depends on how it’s being used. There can be poorly-written TS code and there can be good, just like with JS or any other language.

Often when someone makes a take against TS, we see some badly written code provided as an example. This one specifically was provided online to demonstrate TypeScript’s “unreadability” in general:

type InferArguments<Fun extends (...args: any[]) => any> = Fun extends (...args: infer Args) => any ? Args : never;
type InferCallbackResults<Fun extends (...args: any[]) => any> = Fun extends (...args: any[]) => infer Result
? Result
: never;

function promisify<Fun extends (...args: any[]) => any>(
f: Fun
): (...args: InferArguments<Fun>) => Promise<InferCallbackResults<Fun>> {
return function (...args: InferArguments<Fun>) {
return new Promise((resolve) => {
function callback(result: InferCallbackResults<Fun>) {
resolve(result);
}
args.push(callback);
f.call(null, ...args);
});
};
}

The necessity of such code in your project is questionable in the first place. But let’s just imagine that you do need this code for some reason and only focus on the readability issues.

Things wrong in this code:

  1. InferArguments and InferCallbackResults are redundant. TS has built-in types: Parameters<T> and ReturnType<T>
  2. Verbose parts can be moved into separate types and reused.
  3. Naming arguments with a single letter is a bad idea. For generic type names it’s the opposite — better leave it just as a usual “T”, especially if it’s single.
  4. It’s a bit more concise and natural to use arrow functions when we need them locally & unnamed.

Let’s take a look at my attempt at refactoring it:

type ArgFunc = (...args: any[]) => any;
type PromisifiedFunc<T extends ArgFunc> = (...args: Parameters<T>) => Promise<ReturnType<T>>;

function promisify<T extends ArgFunc>(func: T): PromisifiedFunc<T> {
return (...args: Parameters<T>) =>
new Promise((resolve) => {
const callback = (result: ReturnType<T>) => {
resolve(result);
};
args.push(callback);
func(...args);
});
}

In my opinion, it’s noticeably more readable now. In a single first line of the function, you see all the main info about it — what it accepts and what it returns.

These extracted types can and should be reused in the codebase in every place that has the same types. Your IDE will help you read them. We can also add JSDoc descriptions to make them even clearer.
The “ArgFunc” and “PromisifiedFunc” names are subjective — you can rename them if you want to — the point is that these types do need to be extracted.

Sure, this code can still feel complex, but we will get to that later.

The main take against TypeScript

“Generic types in TS are overly complex, hard to read and understand at a glance. Same code in JS is so much better and concise! And I can unit-test it anyways.”

Generics in TS, as well as in other languages, can indeed be complex and not easy to read. Sometimes it depends on who wrote them, sometimes it is genuinely just how it works.

You can write in plain JS and cover it with tests, but with TS you can catch lots of bugs before even committing anything or running any tests/compilation.

You may have heard some of your colleagues express frustrations like:

“Ugh, why is TS throwing errors at me again?! That would’ve been so much easier in JS…”

However, it’s important to note that in well-written TypeScript code, the presence of these error messages means that you were prevented from committing a bug in the codebase.

Anyway, can we just ditch those syntactically-complex generics and live a better life?

The answer

To answer the question, we must take into consideration the greater scope of things, not just an isolated function.

It feels like this take is viewed within this simplified context:

Isolated utilitary functions
Isolated utility functions

If we compare those functions as in the image above, then sure, the JS variant is much more concise and readable. It’s a straightforward logic, you don’t even really need to test it much.

But now, let’s take a look at the bigger picture:

Utility code affects business logic in the bigger picture

Those complex generics are not just to test this utility function — it’s meant for more — it unlocks the ability to automatically infer types in your business code as you use the utility function, enabling powerful static type-checking capabilities at minimal costs.
This would not be achievable in plain JavaScript — you’ll need to write lots of unit tests or just hope that your QA team will find all bugs.

As you can see, there is a trade-off.

There are 2 types of code in the project

And there is a noticeable difference between them.

  1. Business/App code
    Ideally, most of the time types are automatically inferred. When writing business code in TypeScript, it may be nearly identical to JavaScript, but with the added benefit of extensive IntelliSense support in the IDE. This aids development and helps prevent potential bugs. (Particularly valuable when new developers join a project)
  2. Library/Utility code
    It is widely known that writing libraries in TypeScript is a lot harder than actually using them for the business code in production. Library or utility code often involves complex generics.
    If you need some in your project, it’s best to organize them in a separate “utils” folder or even a separate package, treating them as a “black box.” This code must be as readable as possible, ideally documented with JSDoc.

If you avoid writing complex generics in your util code, the quality of type auto-inference in your business code will suffer.

Or just someone else will write generics for you in the libs you use.

Conclusion

Writing generics in TypeScript can be complex and demanding, but it offers a valuable trade-off.

It’s important to note that complex generics typically reside in utility code rather than throughout the entire project.

When handled skillfully, generics can serve as a powerful tool, enhancing the flexibility and type safety of your codebase.

This is sort of a new skill in the JS world, but the concept has long existed in other strongly-typed languages.

When working with complex generics, it is crucial to handle them carefully by creating a separate abstraction layer, distinct from our business code. By doing so, we can keep our day-to-day code free from bloated generics while leveraging the advantages of static type-checking, thus making our development process smoother and more efficient.

Do you think it’s worth it to maintain complex generics in TypeScript? Please let me know in the comments.

Thank you for reading!

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

--

--

Software Web Engineer at Libertex 👨🏻‍💻. Working with TS 🛡️, JS 🚀, React ⚛️, Vue 🪄, Backbone 🦕