TypeScript Frontend Website Development
TypeScript is not just "JavaScript with types." It's a tool that makes refactoring predictable, documents contracts between modules, and catches an entire class of errors at compile time. For projects with team of two or lifetime longer than a year, TypeScript is not an option, it's a basic requirement.
We use TypeScript as primary language for all frontend: from Vite configuration to API response typing.
Configuration for strict TypeScript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"paths": {
"@/*": ["./src/*"]
}
}
}
strict: true enables 8 flags at once. noUncheckedIndexedAccess adds undefined to type when accessing array by index — half of runtime errors disappear by themselves.
API typing
API response types — one of the first steps on any project. Manual writing is tedious and becomes outdated with backend. Right approach:
OpenAPI → auto-generate types:
npx openapi-typescript https://api.example.com/openapi.json -o src/types/api.ts
Result — precise types for all endpoints. Then type-safe client:
import createClient from 'openapi-fetch';
import type { paths } from '@/types/api';
const client = createClient<paths>({ baseUrl: 'https://api.example.com' });
// TypeScript knows parameter and response types
const { data, error } = await client.GET('/products/{id}', {
params: { path: { id: '42' } }
});
if (data) {
console.log(data.name); // string — without as, without any
}
Patterns for scalable frontend
Discriminated union for loading states:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle': return 'Click to load';
case 'loading': return 'Loading...';
case 'success': return `Loaded: ${JSON.stringify(state.data)}`;
case 'error': return `Error: ${state.message}`;
}
}
Compiler checks exhaustiveness — if add new status, all switch expressions without it become error.
Branded types to prevent ID confusion:
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
function toUserId(id: string): UserId { return id as UserId; }
function getUser(id: UserId): Promise<User> { ... }
const productId: ProductId = toProductId('abc');
getUser(productId); // Compile error: ProductId not compatible with UserId
Zod for runtime data validation:
import { z } from 'zod';
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(255),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'books']),
tags: z.array(z.string()).default([]),
});
type Product = z.infer<typeof ProductSchema>; // Type inferred automatically
// API response validation
const raw = await fetch('/api/products/42').then(r => r.json());
const product = ProductSchema.parse(raw); // Throws if invalid
Linting setup
// .eslintrc or eslint.config.js
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/strict-type-checked",
"plugin:@typescript-eslint/stylistic-type-checked"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-floating-promises": "error"
}
}
no-floating-promises catches incomplete promise chains — entire class of errors that otherwise silently get swallowed.
Framework integration
TypeScript works with any frontend stack:
| Framework | Support level |
|---|---|
| React 18 | Full, JSX via tsx |
| Vue 3 | Full, SFC via <script setup lang="ts"> |
| Svelte 4/5 | Via lang="ts" in script block |
| Solid.js | First-class support |
| Vanilla (no framework) | Full |
Timeline
- Week 1: tsconfig setup, ESLint, type generation from OpenAPI/GraphQL schemas
- Weeks 2–3: component and business logic development with full typing
-
Week 4: code review for
any, refactoring weakly typed places, tests with Vitest
Project delivered with zero any in production code and strict: true enabled. Exceptions documented with explicit comment.







