TypeScript, the good, bad and ugly
JavaScript that finally reads the manual

Contents
JavaScript is famous for being the language that lets you do anything, including all the things you absolutely should not. If you’ve read my take on JavaScript, the good, bad and ugly, you’ll know I have a complicated relationship with it: a genuinely useful language wrapped around a core of decisions that make no sense until you learn the history. TypeScript is JavaScript that has finally sat down and read the manual: same language, same runtime, but with a type system bolted on top that catches your mistakes before your users do. It has gone from a curiosity to the default way large teams write JavaScript, and for good reason. Continuing our series on programming languages, here is TypeScript in three acts.
What it actually is
TypeScript is a superset of JavaScript. Every valid JavaScript file is already valid TypeScript, which is the masterstroke that made adoption possible: you can rename a .js file to .ts and it still works. On top of that base, TypeScript adds static types that the compiler checks at build time and then erases entirely. The output is plain JavaScript with all the type annotations stripped out, because browsers and Node.js have never heard of types and never will. That single design decision, types that exist only at compile time, explains both its greatest strengths and one of its sharpest limitations.
The Good
The headline benefit is static typing over a language that desperately needed it. JavaScript will happily let you call a method that does not exist, pass a string where a number belongs, or read a property off undefined, and you will only find out at runtime, usually in production, usually on a Friday. TypeScript moves that discovery to the moment you type the code:
| |
That typo is caught before the code ever runs. Multiply that across a large codebase and the saved debugging time is enormous.
The tooling is the second great virtue, and arguably the real reason people fall in love with it. Because the editor understands your types, autocomplete becomes genuinely useful, “rename symbol” works reliably across an entire project, and you can refactor with the confidence that the compiler will flag everything you broke. Hovering over a value tells you its exact shape. This is the kind of editor experience that dynamic languages can only approximate.
Adoption can be gradual, which matters for real codebases. You do not rewrite your application; you turn on TypeScript, let everything be loosely typed at first, and tighten the screws over time. Combined with a vast ecosystem of type definitions for popular libraries, you get strong typing without abandoning the npm world you already live in.
There’s a subtler benefit that only shows up months into a project: types are documentation that cannot go stale. A comment saying “this function takes a user object with a name and an email” drifts out of date the moment someone changes the function and forgets the comment. A type signature saying the same thing cannot drift, because the compiler enforces it. New joiners can read the types and trust them, which is not something you can say of prose documentation in any codebase I’ve ever worked in. On a team, that machine-checked contract is often worth more than the bug-catching itself.
The other quiet win is that types make large-scale change safe. Renaming a widely-used field, changing a function’s shape, splitting a module — in plain JavaScript these are acts of faith, and you find out what you broke when a user does. In TypeScript the compiler hands you a to-do list of every call site that no longer type-checks. Refactoring stops being scary, which means it actually happens, which means the codebase stays healthy instead of ossifying because nobody dares touch it.
The Bad
The price of all this is a build step, and with it a configuration burden. JavaScript runs as written; TypeScript must be compiled, which means a toolchain, a tsconfig.json, and a set of decisions about targets, module systems, and bundlers that you would rather not be making. For a small script, the ceremony can outweigh the benefit.
That build step also slows the feedback loop in ways that compound. On a large codebase the compiler can take real seconds to type-check, and while the modern tooling has largely decoupled emitting JavaScript from type-checking it — bundlers strip types in milliseconds and check separately — you still have two processes to keep in sync, and a CI that has to run the slow, thorough check on every commit. It’s a tax you pay forever in exchange for the safety, and on most days it’s worth it, but pretending the tax is zero is how people end up surprised by their build times a year in.
The type system, powerful as it is, can grow baroque. What starts as a tidy interface can metastasise into a thicket of generics, conditional types, and mapped types that nobody on the team fully understands:
| |
That is genuinely useful, but it is also the gateway drug to type definitions that are far harder to read than the actual code they are meant to describe and constrain.
Then there is the escape hatch. The any type switches off type checking entirely for whatever it touches, and it is seductive precisely when you are under pressure and the compiler is in your way. A single stray any can quietly disable safety across a surprising amount of code, and codebases tend to accumulate them in exactly the gnarly corners where types would have helped most.
Finally, and this trips up newcomers constantly: the types are gone at runtime. TypeScript proves things about your code before it runs, but it provides no runtime guarantees whatsoever. Data arriving from an API, a form, or a database has not been checked by anyone, no matter how confidently you typed it.
This is the fundamental difference between TypeScript and a language that was typed from the ground up. In something like Rust, the types are load-bearing all the way down to the machine code; the compiler’s guarantees hold at runtime because the runtime is the typed program. TypeScript’s types are a layer of chalk on top of JavaScript that gets wiped clean before execution. That’s not a flaw exactly — it’s the deliberate trade that made adoption painless — but it means TypeScript’s safety is a development-time property, not a runtime one, and conflating the two is the root of most of the sharp edges below.
The Ugly
The genuinely painful parts of TypeScript live in three places. The first is tsconfig archaeology. The configuration file has accumulated dozens of options over the years, with interactions that are not obvious, and inheriting a project’s config often feels like reading the ruins of decisions made by people who have since left. Flip strict on and a previously green project lights up red; leave it off and you are not getting half the value.
The second is type gymnastics. Because the type system is itself a small, Turing-complete language, people write programs in it. When a library’s types are clever enough, an error message can run to forty lines of nested generic soup that tells you something is wrong without telling you what or where. You came to write a web form; you are now debugging a proof.
The third is the runtime gap made concrete. Because types vanish, you must validate untrusted data yourself, and the temptation is to lie to the compiler instead:
| |
That as does nothing at runtime. If the JSON does not match User, nothing complains until something blows up three functions later. The honest fix is a runtime validation library, which means describing your shapes twice, once for the compiler and once for reality, unless you adopt a tool that derives both from a single schema.
There is also structural typing to surprise you. TypeScript decides two types are compatible if their shapes match, not their names, so a function expecting a Point will happily accept any object that happens to have the right fields, even if it was meant to be something else entirely. Usually convenient, occasionally the source of a bug that no amount of staring at the type definitions will explain.
Closing the runtime gap
The single most valuable habit for serious TypeScript is to validate data at the edges of your program and trust your types everywhere inside. Anywhere untrusted data enters, an HTTP response, a parsed file, a form submission, you should check its shape at runtime rather than asserting it with as. Schema validation libraries make this pleasant by letting you describe a shape once and derive both the runtime check and the static type from the same definition:
| |
Now the compiler’s promise and reality agree, because the data has actually been checked. Adopt that pattern at every boundary and the runtime gap, the sharpest of TypeScript’s ugly edges, mostly closes. Inside that validated core you can lean on the type system with confidence; it is only at the doorways to the outside world that types alone are not enough.
The Verdict
TypeScript is one of the most successful programming-language additions of the last decade, and the enthusiasm is earned. For anything beyond a throwaway script, the combination of early bug detection and first-class tooling pays for itself many times over, and the gradual adoption path means you rarely have to commit all at once. Large teams in particular gain a shared, machine-checked contract about what their code expects, which is worth a great deal when dozens of people are editing the same codebase.
The discipline that keeps it pleasant is restraint. Turn on strict and live with the early pain, treat every any as a small debt to be repaid, validate data at the boundaries instead of asserting it with as, and resist the urge to win awards with your generics. Remember always that the compiler protects you up to the moment your program starts running and not one millisecond after. Used with that humility, TypeScript is JavaScript that has finally read the manual and, mostly, follows it. Used without it, you get all the build complexity and nothing but a comforting illusion of safety that the runtime is under no obligation whatsoever to honour.




