TypeScriptlanguage

Why TypeScript for Web Applications

Type safety that scales from prototype to production

v1.1·10 min read·Kenneth Pernyér
typescripttype-safetyzodvalidationdeveloper-experience

The Problem

JavaScript powers the web, but it was never designed for building large-scale applications. The language's dynamic typing—once a feature for rapid prototyping—becomes a liability as codebases grow.

Every untyped function is a potential runtime error. Every object property access is a gamble. Every refactoring is a prayer that you didn't miss something.

The cost of dynamic typing compounds over time.

When you're exploring, dynamic typing feels liberating. When you're maintaining a system that processes business-critical transactions, it feels reckless. The "flexibility" of any becomes the "fragility" of production bugs.

AI-assisted development makes this worse, not better. AI generates plausible code, but without types, you have no way to verify correctness at the boundaries. The AI doesn't know your domain constraints. Types encode those constraints in a way both humans and AI can check.

We needed a language that:

  • Catches errors before they reach production
  • Documents intent in a machine-verifiable way
  • Enables fearless refactoring with compiler guarantees
  • Provides IDE intelligence that actually understands the code

Current Options

OptionProsCons
JavaScriptThe lingua franca of the web. Maximum flexibility, minimum safety.
  • No compilation step required
  • Runs everywhere (browser, server, mobile)
  • Massive ecosystem and community
  • Low barrier to entry
  • No type safety—errors surface at runtime
  • Refactoring is risky without types
  • IDE support limited without type hints
  • Documentation often incomplete or outdated
TypeScriptJavaScript with types. Gradual adoption, full safety when you need it.
  • Catches errors at compile time
  • Excellent IDE support and autocomplete
  • Types serve as living documentation
  • Gradual adoption—start strict, stay strict
  • Build step required
  • Learning curve for advanced types
  • Can feel verbose for simple scripts
  • Type definitions sometimes lag behind libraries
FlowFacebook alternative to TypeScript. Similar goals, different approach.
  • Designed for gradual typing
  • Good integration with React ecosystem
  • Sound type system by default
  • Much smaller community than TypeScript
  • Tooling ecosystem is limited
  • Facebook has reduced investment
  • Migration path unclear

Future Outlook

TypeScript has won. The question is no longer "should we use TypeScript?" but "how strictly should we configure it?"

The trend is toward stricter configurations. strict: true is the new baseline. Teams are adding noUncheckedIndexedAccess, exactOptionalPropertyTypes, and custom lint rules that enforce even more safety.

Types are becoming mandatory infrastructure.

Modern frameworks assume TypeScript. Bun, Deno, and even Node.js have native TypeScript support or first-class integration. The friction of using TypeScript has dropped to near zero.

AI code generation makes types more valuable, not less. When AI suggests code, types constrain it to produce something that fits your system. Without types, AI generates syntactically valid code that may be semantically wrong.

The future is not just typed JavaScript—it's types as the primary interface between humans, AI, and code.

Our Decision

Why we chose this

  • Compiler catches bugs before productionType errors surface immediately, not in production logs
  • Refactoring with confidenceChange a type, see every affected location instantly
  • Self-documenting codeTypes explain what functions expect and return
  • Superior IDE experienceAutocomplete, go-to-definition, rename symbol all work reliably

×Trade-offs we accept

  • Build step requiredThough Bun makes this nearly instant
  • Advanced types are complexConditional types, mapped types, and infer require study
  • Third-party type quality variesSome @types packages are incomplete or incorrect

Motivation

We write TypeScript for everything. Frontend, backend, scripts, configuration. One language, one type system, one set of patterns.

This isn't about preference—it's about leverage. When a type changes in our API layer, the compiler shows us every component that needs updating. When we refactor a domain model, we know we haven't broken anything.

AI-assisted development amplifies this. We can describe what we want, let the AI generate code, and verify it against our type constraints. Types become a contract between human intent and machine output.

The alternative—hoping that tests catch everything, that documentation stays current, that developers remember all the edge cases—is not a serious option for business-critical systems.

Recommendation

Use TypeScript with the strictest settings you can manage:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Start strict from day one. It's much harder to add strictness later than to maintain it from the beginning.

For new projects, use Bun or Deno for native TypeScript execution. For existing Node.js projects, use tsx or ts-node with SWC for fast transpilation.

Use Zod for runtime validation at system boundaries.

TypeScript types exist only at compile time—they're erased at runtime. When data crosses trust boundaries (API responses, user input, file parsing), you need runtime validation. Zod bridges this gap: define a schema once, get both runtime validation and TypeScript types.

Zod is to TypeScript what Pydantic is to Python. It eliminates the "I defined types but the API returned something different" class of bugs. Parse, don't validate—if it passes Zod, you know the shape is correct.

Examples

src/domain/money.tstypescript
// Branded types for domain safety
type Brand<T, B> = T & { __brand: B };

type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;

// Compiler prevents mixing currencies
function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD;
}

const dollars = 100 as USD;
const euros = 50 as EUR;

addUSD(dollars, dollars); // ✓ OK
addUSD(dollars, euros);   // ✗ Type error!

Branded types encode domain rules in the type system. The compiler enforces constraints that would otherwise require runtime checks.

src/api/schemas.tstypescript
import { z } from 'zod';

// Define schema once, get runtime validation + TypeScript types
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1).max(100),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.string().datetime(),
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;

// Parse untrusted data at system boundaries
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();

  // Throws if data doesn't match schema
  // Returns fully typed User if it does
  return UserSchema.parse(data);
}

// Safe: compiler knows user.role is 'admin' | 'user' | 'guest'
const user = await fetchUser('123');
if (user.role === 'admin') {
  // TypeScript knows this is valid
}

Zod validates data at runtime and infers TypeScript types from schemas. Define once, get both compile-time and runtime safety. This is how you handle API responses, form data, and any untrusted input.

Related Articles

Stockholm, Sweden

Version 1.1

Kenneth Pernyér signature