Catching Bugs Before They Happen
ESLint, Prettier, and TypeScript are staples of the modern JavaScript toolchain, but they're rarely thought of as testing tools. That's a mistake. Each one performs a form of static analysis—examining code without executing it—that can catch an entire category of errors before they ever reach a user.
Linting: Automating Code Review
ESLint scans JavaScript for potential problems without running it. Consider the following snippet:
if ((!'serviceWorker') in navigator) {
// the user's using an old browser :-(
}
You can probably spot the issue yourself, but relying on human attention is fragile. A linting tool catches this pattern every time, no matter how tired or distracted the developer is. It's the difference between hoping nobody makes a mistake and guaranteeing the mistake is flagged the moment it's introduced.
Formatting: Making Intent Visible
Prettier is typically thought of as a formatter, not a test. But formatting has a direct impact on correctness. Take this expression:
const a = false
const b = false
const c = true
const d = a && b || c
What is the value of d? Unless you have the JavaScript operator precedence table memorized, it's not immediately obvious. And even if you do, you can't assume every teammate does. After running the code through Prettier, it becomes:
const a = false
const b = false
const c = true
const d = (a && b) || c
Prettier adds the parentheses automatically, making the evaluation order explicit. If that's not the intended logic, you can write the expression with your own parentheses—const d = a && (b || c)—and Prettier will leave them alone. The formatter doesn't just make code consistent; it surfaces the author's intent and removes ambiguity that often hides bugs.
Types: Inline Automated Tests
TypeScript adds static type checking to JavaScript, letting you declare what kind of data a variable holds and verify it's used correctly throughout the code. The days of x is not a function errors suddenly become rare. Consider this JavaScript function:
function getFullName(user) {
const {
name: { first, middle, last },
} = user
return [first, middle, last].filter(Boolean).join(' ')
}
getFullName({ first: 'Joe', middle: 'Bud', last: 'Matthews' })
There's a bug in there—but it's easy to miss. Rewriting the function in TypeScript changes that:
type User = {
name: {
first: string,
middle: string,
last: string,
},
}
function getFullName(user: User): string {
const {
name: {first, middle, last},
} = user
return [first, middle, last].filter(Boolean).join(' ')
}
Now the compiler catches the mistake when the function is called:
Argument of type '{ first: string; middle: string; last: string; }' is not assignable to parameter of type 'User'.
Object literal may only specify known properties, and 'first' does not exist in type 'User'.(2345)
Type definitions act as inline automated tests. They verify not just that code runs, but that it's used as intended. You can adopt TypeScript incrementally, even alongside Babel using babel-preset-typescript. Try it on your next feature—the errors the compiler catches will likely convince you quickly.
A Foundation for Confidence
Static analysis provides a fast, low-effort boost in confidence compared to writing unit tests for everything. It runs in milliseconds, integrates into editors and CI pipelines, and covers patterns developers might never think to test. ESLint, Prettier, and TypeScript form the base of the Testing Trophy approach to software quality—and they're tools worth adopting now, not later.



