Before I start, I would like the jury to know that I am, for the most part, a TypeScript fan. It is my primary programming language for frontend React projects and for any backend Node work that I do. I am on board, but I do have some nagging doubts that I would like to discuss in this post. I have programmed in nothing but TypeScript for at least three years now across numerous contracts, so TypeScript is doing something right or is fulfilling a need.
TypeScript has defied some insurmountable odds to become mainstream in the realm of frontend programming. TypeScript is no.7 in this post that lists the most in-demand programming languages.
Whether you use TypeScript or not, the following practices should be in place for any software team irrespective of size:
TypeScript can add an extra layer of safety on top of these, but I feel it’s last by a country mile in the list of in-demand programming languages.
I think this is possibly the main problem with the current incarnation of TypeScript but, first of all, let me define what sound and unsound type systems are.
A sound type system is one that ensures your program does not get into invalid states. For example, if an expression’s static type is string
, at runtime, you are guaranteed only to get a string
when you evaluate it.
In a sound type system, you should never be in the position at compile-time OR RUNTIME where the expression does not match the expected type.
There are, of course, degrees of soundness and soundness is open to interpretation. TypeScript is sound to a certain degree and catches the type errors below:
// Type 'string' is not assignable to type 'number' const increment = (i: number): number => { return i + "1"; } // Argument of type '"98765432"' is not assignable to parameter of type 'number'. const countdown: number = increment("98765432");
Typescript is entirely upfront about the fact that 100% soundness is not a goal and non-goal no.3 on the list of non-goals of TypeScript clearly states:
Apply a sound or “provably correct” type system. Instead, strike a balance between correctness and productivity.
What this means is that there is no guarantee that a variable has a defined type during runtime. I can illustrate this with the following somewhat contrived example:
interface A { x: number; } let a: A = {x: 3} let b: {x: number | string} = a; b.x = "unsound"; let x: number = a.x; // unsound a.x.toFixed(0); // WTF is it?
The above code is unsound because a.x
is known to be a number from the A
interface. Unfortunately, after some reassignment shenanigans, it ends up as a string, and the following code compiles but errors at runtime.
Unfortunately, the expression shown here compiles without any error:
a.x.toFixed(0);
I think this is possibly the biggest problem with TypeScript, as soundness is not a goal. I still encounter many runtime errors that are not flagged by the tsc
compiler that would be if TypeScript had a sound system. TypeScript has one foot in both the sound and unsound camp with this approach. This halfway house approach is enforced with the any
type, that I’ll mention later.
I still have to write just as many tests, and I find this frustrating. When I first started getting TypeScript, I wrongly concluded that I could stop the drudgery of writing so many unit tests.
TypeScript challenges the status quo, claiming that lowering the cognitive overhead of using types is more important than type soundness.
I understand why TypesScript has gone down this path, and there is an argument that states that the adoption of TypeScript would not have been as high if a sound type system is 100% guaranteed. This is disproved as the dart language is finally gaining popularity as Flutter is now widely used. Soundness is a goal of the dart language which is discussed here.
Unsoundness and the various ways that TypeScript exposes an escape hatch out of strict typing make it less effective and unfortunately make it better than nothing right now. My wish is that, as TypeScript gains popularity, more compiler options are available to enable power users to strive for 100% soundness.
Runtime type checking is not one of TypeScript ‘s goals, so this wish will probably never happen. Runtime type checking would be beneficial when dealing with JSON payloads returned from API calls, for example. A whole category of errors and many unit tests would not need to exist if we could control this at the type level.
We cannot guarantee anything at runtime so this might happen:
const getFullName = async (): string => { const person: AxiosResponse = await api(); //response.name.fullName may result in undefined at runtime return response.name.fullName }
There are some supporting libraries like io-ts, which is great but can mean you have to duplicate your models.
any
type and strictness optionThe any
type means just that, and the compiler allows any operation or assignment.
TypeScript tends to work well for small things, but there is a tendency for people to any
type anything that takes longer than one minute. I recently worked on an Angular project and saw a lot of code like this:
export class Person { public _id: any; public name: any; public icon: any;
TypeScript is letting you forget the type system.
You can blast the types out of anything with an any
cast:
("oh my goodness" as any).ToFixed(1); // remember what I said about soundness?
The strict
compiler option enables the following compiler settings which do make things more sound:
--strictNullChecks
--noImplicitAny
--noImplicitThis
--alwaysStrict
There is also the eslint rule @typescript-eslint/no-explicit-any.
The proliferation of any
can destroy soundness in your typing.
I must reiterate that I am a TypeScript fan, I use it on my day to day job, but I do feel it is coming up short and the hype is not entirely justified. Airbnb claims 38% of bugs could have been prevented by TypeScript. I am very skeptical of this precise percentage. TypeScript is not turbocharging existing good practices. I still have to write just as many tests. You could argue, I am writing more code, and I might have to write type tests. I am still getting unexpected runtime errors.
TypeScript offers above basic type checking, but soundness and runtime type checking are not goals, and this leaves TypeScript in an unfortunate halfway house with one foot in a better world and one where we currently are.
Where TypeScript shines is with good IDE support, like vscode, where we get visual feedback if we mistype something.
Refactoring is also enhanced with TypeScript and breaks in code (such as changes in method signatures) are instantly identified when running the TypeScript compiler against the modified code.
TypeScript has enabled good type checking and is definitely better than no type checker or just plain eslint but I feel it could do much more and sufficient compiler options could be exposed to those of us who want much more.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
11 Replies to "Is TypeScript worth it?"
Typescript will never be sound until it’s a complete interpreter.
TypeScript’s type system isn’t sound, but it sure is pragmatic. I love the balance they strike with it tbh.
Great article. Just one small nit… You say
You can blast the types out of anything with an any cast
Typescript calls these Type Assertions, not casts, as they are slightly different. From their docs:
Type assertions are a way to tell the compiler “trust me, I know what I’m doing.” A type assertion is like a type cast in other languages, but performs no special checking or restructuring of data. It has no runtime impact, and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.
https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions
yep agreed, not having runtime checking is a huge weakness but if you remember JS world before FlowJS and TypeScript it’s definitely a huge leap in the right directionb.
Give https://elm-lang.org/ a go, you might be pleasantly surprised 🙂
“TypeScript does not guarantee any runtime type checking”
This is also true if you rely on external Javascript libraries which are delivered with inaccurate types (not auto generated). Another true negative in safety.
I think you are right. I stated in the article I am a fan. I just wish I had some more knobs (compiler options) to make things more strict.
I’m concerned that it is dividing our community and adding to workflow (JS repos needing to add TS constructs).
I think the right solution will eventually be WASM, where TS would cleanly separate from JS.
> I think this is possibly the biggest problem with TypeScript, as soundness is not a goal. I still encounter many runtime errors that are not flagged by the tsc compiler that would be if TypeScript had a sound system.
You’re basically alluding to having the same capacity as Rust’s borrow checker. This is a tall order when ultimately the runtime model of the tsc output is JS (…..a dynamically typed language). I’m not a compiler engineer, but I don’t even know if that’s possible to do at all.
Obviously tsc was never going to be sound. It was a nonstarter from the beginning.
the failing examples are basically “typecasts” e.g. let x: number = a.x; // unsound. best to use type inferrence instead. if you use typescript’s type inferrence to the maximum extent, you will catch more errors. doing explicit typing forces typescript to say “ok you’re the boss”. one big complaint I have is @typescript-eslint has a default called “explicit-function-return-type” https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-function-return-type.md it’s actually better if you don’t specify the explicit return type, because you will literally fail to create a better explicit return type in your code than the typescript’s inferrence can. furthermore, if you do need it explicitely somewhere, you can say ReturnType
> What this means is that there is no guarantee that a variable has a defined type during runtime.
Kind of a difficult thing to blame TypeScript for. After all, it’s based on the abomination called JavaScript, which doesn’t give a shit about any practice established decades ago. Now the web dev community is rediscovering these things instead of just throwing the bullshit language out of the window once and for all.
bUt MaH bAckWarD cOmPatIbIliTy aNd bReAkInG thE wEb!!!11 Imagine where the gaming industry would be today and what kind of games we’d play if we cared that they worked on Commodore 64. Because, you know, 0.001% of people might still use it and you need to be inclusive in your design and all that. You have system requirements for certain type of things. Wanna play this game? You need at least 4GB of RAM, a solid GPU and (probably) Windows. Or a console. But you wanna look at this website on your Internet Explorer 11 and Windows XP? Sure, lemme completely destroy the code in order to allow you to open it!
Drop JavaScript, create a proper language which supports what UI development needs and we’ll stop having a blog post every day about TypeScript vs Flow, React vs Angular, etc. Instead we’ll simply use the thing, instead of building a whole bunch of tools just to forget the fact that under the hub it’s all JavaScript.
To be fair, the root of the problem is JS itself.