TypeScript
Tool
A language that adds type annotations to JavaScript so mismatched values get caught before the code ever runs.
Why it exists
JavaScript does not ask you to declare what kind of value anything holds. Put a number in a variable, then put text in the same variable, and nothing is wrong with the syntax. What a function accepts and what it returns is not written down anywhere in the code either. With twenty files and two people, that freedom is what makes it quick to write in.
At scale it works the other way. Once there are 800 files and fifteen people, the only way to know what to pass into somebody else's function is to go read it. Theo hit this directly: calling a function that returns user information, he wanted the company name out of it, did not know what the field was called, and traced back through three files to find out.
The worse case is the error that passes silently. Hand text to code that adds amounts together and JavaScript does not stop — it concatenates instead. Three and five become 35 rather than 8, and nobody knows until the value reaches a screen. The place where mistakes surface had drifted from the editor out to a customer's screen after release, and TypeScript exists to pull that point back.
What it solves
It makes you write down what kind of value each thing holds, then flags the mismatches before anything runs. Pass text where an amount belongs and a red line appears in the editor the moment you save; leave it there and the build fails. This check is a form of static analysis — inspecting code for problems without executing it.
Three things come back from that.
- Errors move earlier. The same mistake that used to arrive as a support ticket after release now arrives while you are typing. It matters because what a fix costs depends heavily on when it is found: a red line takes seconds to clear, while the same mistake found in production costs a ticket, a diagnosis, and a release.
- A function's contract lives in the code. That this function takes a user id and returns a name and a company name is recorded as code rather than as a comment. Comments can go stale with no consequence; a declaration that stops matching blocks the build.
- Changes become safer. Rename the company name field on user information and all forty places using it light up red. Confirming you did not miss one no longer means clicking through screens.
Editor autocomplete comes out of the same information. Because the kind of value is known, typing a dot brings up the list of fields it actually has.
There is one boundary worth knowing. The annotations are used during checking and do not exist in what ships. Browsers still run JavaScript. So a response from a server that does not match its declared shape is not caught at runtime, and values arriving from outside need explicit checking code of their own.
Alternatives
Four options overlap with it, and all four tend to divide the work into layers rather than replace one another.
- Stay on plain JavaScript and record the contract in comments. Writing above a function what it takes and returns adds no build step and costs little to learn. It also has no enforcement, so comments and code can sit out of sync for months.
- Use a linter, which automatically catches rule violations — unused variables, missing error handling, the habits that tend to produce failures. It cannot see the kind of a value, so it does not overlap with TypeScript, and most teams run both.
- Add runtime validation. Check that a server response or a user input has the shape you expected at the moment it arrives, and stop there if it doesn't. It covers exactly the gap TypeScript leaves, which makes the two complements rather than options. In practice teams validate at the edges — API responses, form submissions, uploaded files — and trust the declared types everywhere inside.
- Write more unit tests — checks that feed a function a value and compare the result against an expected one. Types ask whether the shape is right and tests ask whether the result is right, so what they prevent is different.
Scale and lifespan decide it. On a tool two people will use for two months, the setup is the larger cost; on a product fifteen people will edit for three years, what gets filtered out in advance is worth far more. Nobody regrets it at 800 files, and plenty of teams regret adding it to a prototype that shipped once.
How it sounds at work
"The argument type on this function is too loose. Anything at all gets through."
A declaration that permits everything, which turns the check off in practice.
"Let's write new files in TypeScript and move the old ones as we touch them."
Migrate gradually rather than converting everything at once.
"I declared the type for the server response, and the real response didn't have that field."
A declaration is for checking and guarantees nothing at runtime.
Pagedeck writes all of its frontend code in TypeScript. Theo introduced it without a big-bang conversion: new files used it, and older files moved over whenever someone opened them for other reasons. Six months in, fewer than ten JavaScript files were left. The build was configured to allow both from the start, which is what made the slow version possible.
The effect shows up most clearly when Noa changes something in the design system. Rename a button size and every screen using it turns red, which produces a list of exactly what needs checking. The same job used to come with half a day of clicking through screens one at a time.
What people get wrong
The most common mistake is expecting types to remove bugs. Red lines really do catch a lot of errors, so the experience reads that way. But this check only looks at the shape of a value. Multiply where you meant to add a discount rate and, as long as both sides are numbers, nothing happens at all. Left in place, the belief leads teams to write fewer tests because types are in, while the incidents keep coming from the arithmetic. A discount applied twice is a perfectly typed number.
The second is assuming a declared type holds at runtime. It is written in the code, so it feels like it travels all the way. But the declaration disappears once checking is done, and what reaches the browser is JavaScript. A response missing a promised field runs straight through, and the screen renders a blank where that value should be. Values from outside need runtime checks, and teams holding this belief skip writing them.
Easily confused with
| Term | What it is | How it differs |
|---|---|---|
| JavaScript | The language that implements behavior in the browser | It is the side that executes. TypeScript code is turned into JavaScript before it runs |
Where to go next
Last updated 2026-09-16