TypeScript or Tears
Source: Dev.to
See also: Backend Quality Gates
Backend linters catch async footguns. Type checkers prevent runtime explosions. Now it’s the frontend’s turn.
The Problem
JavaScript fails silently. That’s the whole problem in one sentence.
- You call a function with the wrong arguments → it runs.
- You access a property that doesn’t exist → it runs.
- You forget to handle
null→ it runs.
Everything runs. Nothing works. Users complain. You have no idea why.
“This is fine. Everything is fine.”
— Just kidding. This is chaos.
Like the backend, none of this is even testing yet. We’re only checking that the code isn’t obviously broken before we bother running actual tests. The bar is still on the floor. Let’s at least step over it.
Why JavaScript Needs Types
JavaScript has no types. This was a design decision. It was wrong.
function processUser(user) {
return user.name.toUpperCase();
}
What is user? An object? A string? A promise? JavaScript doesn’t know. JavaScript doesn’t care. JavaScript believes anything is possible.
JavaScript is an optimist. You shouldn’t be.
Enter TypeScript (strict mode)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Now the compiler yells at you:
// TypeScript rejects this
function processUser(user) { // Error: implicit 'any'
return user.name.toUpperCase();
}
// TypeScript accepts this
function processUser(user: User): string {
return user.name.toUpperCase();
}
Is it annoying? Yes.
Does it catch bugs before users do? Also yes. That’s the trade‑off.
I use strict: true and noUncheckedIndexedAccess. The latter is particularly annoying because it assumes array access might return undefined—and it can, so you should handle it.
Linting with ESLint
TypeScript catches type errors. ESLint catches everything else:
- Unused variables
- Inconsistent formatting
- Dangerous patterns
- Forgotten
console.login production
I use the Airbnb style guide. It’s opinionated, strict, and battle‑tested. Thousands of engineers have argued about these rules so I don’t have to.
// eslint.config.js
import { configs, extensions, plugins } from 'eslint-config-airbnb-extended';
export default [
plugins.stylistic,
plugins.importX,
plugins.react,
plugins.reactA11y,
plugins.reactHooks,
plugins.typescriptEslint,
...extensions.base.typescript,
...extensions.react.typescript,
...configs.react.recommended,
...configs.react.typescript,
];
One import. Hundreds of rules. TypeScript support, React hooks, accessibility, import ordering—all pre‑configured.
no-explicit-any is the important one. It closes the escape hatch: you can’t just type any and pretend you have type safety. Either type things properly or the build fails.
Some people find this restrictive. Those people debug production issues at 2 AM. I watch Netflix at 2 AM. Different choices.
Documenting Component States with Storybook
Here’s a fun game: refactor a component, run the app, click around, ship it. Three months later, discover you broke the loading state on a page nobody visits often.
Components have many states:
- Happy path
- Error state
- Loading state
- Empty state
- Edge cases
You can’t manually test all of them every time. You won’t. I won’t. Nobody will.
Storybook documents every state:
// Button.stories.tsx
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Loading: Story = {
args: {
isLoading: true,
children: 'Loading...',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Nope',
},
};
Run build-storybook in CI. If any component can’t render in any state, the build fails. You broke something—fix it before it ships.
Bonus: AI can read these stories, understand what states exist, and generate code that handles them. Documentation that actually gets used.
One Job per Check
When something fails, you know exactly what.
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
Three jobs. Same stage. Run in parallel.
- The hidden job (
.frontend-quality) starts with a dot, so GitLab treats it as a template (it won’t run directly). npm ci– notnpm install.ciis faster, stricter, and uses the lockfile exactly. No surprises.--prefer-offline– use cached packages when possible. Network is slow; cache is fast.
Parallel Execution
All three jobs run at the same time:
- ESLint fails? You see it immediately.
- TypeScript fails? You see both. Fix them together.
- Storybook build fails? You see it too.
Storybook artifacts keep the built Storybook around. when: always saves it even if the build fails—useful for debugging why a component broke.
When one job fails, you see exactly which one in the pipeline view. No scrolling through logs. No guessing.
| Job | Failure Means |
|---|---|
eslint | Style issues or dangerous patterns |
typecheck | Type errors |
storybook:build | Component can’t render in a story |
None of this verifies that the code does what it should—that’s what tests are for. This just verifies it’s not obviously broken.
The bar is low, but you’d be surprised how many projects can’t clear it.
Full CI Configuration
stages:
- quality
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
Copy, paste, adapt. It works.
Frontend code fails in creative ways: silent failures, runtime errors, “It works on my machine” but not in Safari, components that render fine until you pass unexpected props.
I can’t catch all of this manually. My attention span isn’t that good. Nobody’s is.
So I automate the obvious stuff:
- Types must be explicit
- Code must follow consistent patterns
- Components must render without crashing
The machines catch what I miss. The pipeline blocks what I’d regret.
Same deal as the backend: write the rules once, enforce them forever.
Next up: Security – coming soon – Dependencies are other people’s code. And other people make mistakes.