Let me say the quiet, slightly heretical thing out loud: we write plain JavaScript. On purpose. In 2026. Across every project. No .ts files, no compile step for types, no transpile pipeline standing between the code and the runtime.
I know how that sounds. The 2026 consensus is settled and loud: TypeScript won, plain JavaScript is a legacy choice, and any professional still shipping .js is either behind or careless. I have read the “we ditched JavaScript on day 30” posts. And I still made this call deliberately, with eyes open - because the choice people think they are making (“types or no types”) is not the choice that is actually on the table.
The false binary
Here is the assumption baked into every “just use TypeScript” thread: that plain JavaScript means no types. Untyped, unchecked, a runtime full of undefined is not a function waiting to happen.
That has not been true for years. You can have real static type checking - the kind that fails your CI when you pass a string where a number belongs - without a single .ts file, without a build step, without changing how your code runs. The tool that does it is the TypeScript compiler itself, pointed at your JavaScript.
The trick is JSDoc plus two flags. You annotate with comments the language already ignores, turn on checking, and let tsc verify without emitting anything:
// @ts-check
/**
* @param {number} price
* @param {{ taxRate: number }} opts
* @returns {number}
*/
export function withTax(price, opts) {
return price * (1 + opts.taxRate);
}
withTax("10", { taxRate: 0.19 });
// ^ Argument of type 'string' is not assignable to parameter of type 'number'.
Your editor shows the same red squiggle it would for TypeScript. And in CI, one command guards the whole codebase - no output, just verification:
// tsconfig.json — type-check .js, emit nothing
{
"compilerOptions": { "checkJs": true, "noEmit": true, "strict": true },
"include": ["src/**/*.js"],
}
tsc --noEmit # fails the build on a type error, ships zero files
That is the part the binary hides. The real choice is not “types vs. no types”. It is “types with a build pipeline” vs. “types with a comment and a checker”. And once it is framed that way, it becomes an engineering trade-off - not an obvious win.
What we actually gain
Choosing the JSDoc side buys concrete things, and they compound:
- No build step for types. The code you write is the code that runs. No transpile, no source maps to untangle when a stack trace points at line 4000 of a bundle, no “works in dev, breaks in the build” type-only weirdness.
- It runs anywhere, instantly. Any V8 engine, a
<script>tag, a quick Node one-off - nots-node, no loader, no toolchain to install before the first line executes. - A lower barrier. New contributors read exactly what runs. The mental model is one layer shallower, which matters more than seniors like to admit.
- Fewer moving parts. This is really a KISS decision. Every tool you do not add is a tool that cannot break, cannot need upgrading, and cannot own an afternoon during a version bump.
None of these are dramatic on their own. Together they are the difference between a toolchain you maintain and a toolchain that maintains you.
The honest costs
A post that only listed upsides would be exactly the kind of dishonest hype I complain about. JSDoc has real limits, and you should know them before you copy this:
- Complex types get verbose. Simple annotations are clean. Advanced generics, mapped types, and conditional types are expressible in JSDoc but grow ugly fast - what is one tidy line in TypeScript becomes a multi-line comment you do not enjoy writing.
- Refactoring tooling is weaker. Rename-across-project and automated refactors work more reliably on real TypeScript. On JSDoc JavaScript they are good, not great.
- Cross-module inference is thinner. TypeScript’s editor experience - auto-imports, deprecation warnings inline, tracking a type across boundaries - is a class above JSDoc inference. You feel it most in very large codebases.
I am not going to pretend those do not exist. For the code we write - applications, not published libraries; pragmatic domain types, not type-level gymnastics - they are papercuts, not blockers. For other code, they might be dealbreakers. Which is the actual point.
When I would reach for TypeScript anyway
This is a contextual decision, not a crusade. I would drop the JSDoc approach and use real .ts without hesitation when:
- You are authoring a library whose consumers depend on shipped, first-class type definitions.
- The domain is genuinely type-heavy - complex generics, discriminated unions everywhere, types that are load-bearing logic.
- The team is large and leans hard on automated refactoring across a big surface, where TypeScript’s tooling edge pays for its build cost daily.
- The team already knows and prefers TS - developer happiness and consistency are real engineering inputs, not footnotes.
Notice that none of those criteria is “because it is 2026 and everyone says so”. That is the only bad reason - and it is the most common one.
It is a values choice, not a compatibility one
Strip away the tribalism and this is not really about JavaScript or TypeScript. It is about what you optimize for. TypeScript optimizes for scale, refactorability, and catching a wider class of errors at authoring time - and pays for it with a build step and a toolchain. Plain JavaScript with JSDoc optimizes for simplicity, immediacy, and a shallow stack - and pays for it with verbosity at the edges and weaker refactor tooling.
Both are legitimate. What is not legitimate is adopting the more complex option reflexively because a headline told you the debate is over. The debate is over for some contexts. Whether yours is one of them is a decision you are supposed to make, not inherit.
Tea and the espresso machine
A commercial espresso machine is an extraordinary piece of engineering - pressure control, temperature stability, a shot dialed to the gram. In a busy café serving three hundred coffees a day, it is exactly the right tool, and running one is a craft worth mastering.
Now put that same machine in a kitchen where someone wants one cup of tea in the morning. It still works. It is still “better” by every spec on the box. But now there is a machine to descale, parts to replace, a warm-up ritual, and a manual to consult - all to boil water you could have boiled in a kettle in ninety seconds. The espresso machine did not get worse. The context changed, and the “obviously superior” tool became overhead.
TypeScript is a superb espresso machine. Sometimes you are running a café, and you should absolutely use it. And sometimes you just want a good cup of tea - typed, checked, and served without a machine to maintain. Knowing which kitchen you are in is the whole job. Refusing to check who is asking, and reaching for the espresso machine every time because the internet said so - that is not engineering. It is just following the crowd with extra steps.