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

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

References