TypeScript
TypeScript is a statically typed superset of JavaScript created by Microsoft. Every JavaScript program is (almost) a valid TypeScript program; TypeScript adds optional type annotations and a compiler that checks them, then emits plain JavaScript that runs anywhere JavaScript runs. Types disappear at runtime — their value is in catching mistakes before code ships and in powering editor features like autocomplete, go-to-definition, and safe refactoring.
TypeScript has become the default for serious JavaScript work: React, Angular, Vue, Node.js backends, Deno, and Bun all treat it as a first-class language, and most popular npm packages ship type definitions. Recent runtimes can even run .ts files directly by stripping types, so the build step is increasingly optional.
TL;DR
- TypeScript adds static types to JavaScript; the compiler checks them and emits plain JS.
- The type system is structural: compatibility depends on shape, not declared names.
- Inference does most of the work — annotate function boundaries and public APIs, not every variable.
- Union types plus narrowing model real data safely (
string | null, discriminated unions). - Generics let functions and types work over many types while staying precise.
- Enable
strictmode, avoidany, and validate external data at runtime (e.g. with Zod).
Quick Example
A discriminated union that makes invalid states unrepresentable, a generic helper, and exhaustive handling:
Add a new status to RequestState and the compiler points to every switch that needs updating.
Core Concepts
Structural Typing and Inference
If an object has the properties a type requires, it's compatible — no implements needed. The compiler infers types from initializers, return statements, and context:
Interfaces vs Type Aliases
Many teams use interface for object shapes and type for everything else; consistency matters more than the choice.
Unions and Narrowing
A union (string | number) means "one of these." TypeScript narrows it inside control flow using typeof, instanceof, in, equality checks, and custom type guards (function isUser(x): x is User). Discriminated unions — a shared literal field like status — are the most reliable way to model state machines and API responses.
Generics
Generics parameterize types: Array<T>, Promise<T>, Map<K, V>. Constraints (T extends { id: string }) restrict what can be passed; defaults (T = unknown) simplify usage. Good generics connect inputs to outputs so callers get precise types back.
Utility and Advanced Types
Built-in helpers transform types: Partial<T>, Required<T>, Pick<T, K>, Omit<T, K>, Record<K, V>, ReturnType<F>, Awaited<P>. Mapped types, conditional types, template literal types, and the satisfies operator enable powerful patterns. See TypeScript Advanced Features.
The Type System Isn't a Runtime
Types are erased when compiled. Data from fetch, JSON.parse, forms, environment variables, or databases is unknown in reality, whatever you annotate. Validate it at the boundary with a schema library like Zod, which also infers the TypeScript type from the schema.
Compiler Configuration
Key tsconfig.json options:
Best Practices
Turn On Strict Mode From Day One
Retrofitting strictNullChecks into a large codebase is painful. New projects should start strict.
Prefer unknown Over any
unknown forces you to narrow before use; any silently disables checking and spreads through the codebase.
Type the Boundaries
Annotate exported functions, API responses, and component props explicitly. Let inference handle local variables.
Model States, Not Flags
Replace combinations like isLoading, isError, and data? with a discriminated union so impossible combinations can't exist.
Use satisfies for Checked Literals
const routes = { home: "/", about: "/about" } satisfies Record<string, string> validates the object while keeping its precise literal types.
Run the Type Checker in CI
Bundlers like Vite and esbuild strip types without checking them. Run tsc --noEmit (or tsc -b) as a CI step.
Common Mistakes
Reaching for any
Trusting Types at Runtime
Casting API responses with as User doesn't check anything. If the server changes shape, the bug surfaces far from its cause.
Non-Null Assertions Everywhere
value! tells the compiler to trust you. Frequent use usually means the types don't reflect reality — fix the types or handle the null case.
Over-Engineered Types
Deeply nested conditional types can make errors unreadable and slow the compiler. Prefer simple, explicit types unless a library genuinely needs the flexibility.
Enums With Surprising Behavior
Numeric enums allow any number and generate runtime code. Union types of string literals ("admin" | "user") or as const objects are usually simpler.
FAQ
Why use TypeScript over plain JavaScript?
It catches whole classes of bugs — typos, missing null checks, wrong argument types — before code runs, makes refactoring safe, documents intent, and gives much better editor support. The benefits grow with codebase and team size.
Interface or type alias?
Both describe object shapes. Use interface when you need declaration merging or prefer its extends syntax; use type for unions, tuples, and computed types. Pick a convention and apply it consistently.
Does TypeScript catch all bugs?
No. It catches type errors, not logic errors, and it can't validate data arriving at runtime. You still need tests and runtime validation at system boundaries.
Do types exist at runtime?
No. Types are erased during compilation. Use schema validation libraries if you need runtime checks.
How do I migrate a JavaScript project to TypeScript?
Add a tsconfig.json with allowJs, rename files to .ts incrementally starting with leaf modules, add types at boundaries, install @types/* packages, and tighten strictness settings over time.
Related Topics
- JavaScript — The language TypeScript extends
- TypeScript Basics — Getting started with types and configuration
- TypeScript Advanced Features — Generics, conditional and mapped types
- Zod — Runtime validation with inferred types
- Type Systems — The theory behind static typing