TypeScript: Complete Guide
TypeScript is JavaScript with a type checker in front of it. You write down what your code expects, and the compiler and your editor complain when the code drifts away from that shape. What ships is still JavaScript, and the browser never sees a type.
This page is the map of the TypeScript section. It covers what the language adds, the one rule every guide below assumes, and where each of the eleven guides fits. Skip to the table if you already know what you came for.
TypeScript at a Glance
The section splits into three topics, roughly in the order most people meet them.
| Area | What it covers | Start here if |
|---|---|---|
| Types | Annotations, interfaces, unions, runtime data | You are writing your first TypeScript |
| Generics | Function types, type parameters, utility types | You keep repeating the same shapes |
| Tooling | tsconfig.json, checking, React, API contracts | The build or the config fights you |
What TypeScript Actually Adds
TypeScript adds a static type checker on top of JavaScript. When you invoke it, through tsc, a CI step, or your editor's language service, it models the values in your files and reports where that model does not line up. Nothing checks your types unless something asks it to, which is why transpiling is not type checking.
type User = {
id: string;
email: string;
displayName: string | null;
};
function formatUserLabel(user: User): string {
return user.displayName ?? user.email;
} Nothing clever is happening, which is the point. The User type names a shape once, and under strictNullChecks the compiler makes the function handle the null case. The return annotation tells every caller TypeScript checks what comes back.
Three words carry the rest of this section: a type describes the shape of a value, an annotation is where you write one down, and inference is the compiler filling one in for you.
Types Are Erased Before the Code Runs
Here is the rule every guide in this section assumes and none of them stops to argue. Types belong to the checker, not to the running program: what the compiler emits is plain JavaScript with the annotations stripped out. They can outlive the build in a separate .d.ts file, but never inside the code that executes.
// input: user.ts
type User = { id: string; name: string };
const user: User = { id: "u1", name: "Ada" };
console.log(user.name); // output: user.js, after running tsc with an ES2015 or newer target
const user = { id: "u1", name: "Ada" };
console.log(user.name); // prints: Ada The declaration and the annotation vanished. Type-only syntax like that costs nothing at runtime, and it equally cannot vouch for data that only arrives at runtime. Other compiler settings do change the emitted code: an older target downlevels syntax and can add helper functions, and the JSX and decorator-metadata options emit code of their own.
A few constructs are exceptions worth knowing before they surprise you. A regular non-ambient enum, a namespace holding runtime values, parameter properties, and import = or export = all emit real JavaScript. Const enums inline instead unless preserveConstEnums is set, ambient declarations emit nothing, and the rest of the type layer is erased.
The consequence shows up at every boundary. Parsed JSON, form input, and webhook payloads arrive unvalidated, and no annotation checks them. A runtime check is the answer; unknown is the right starting type when the shape is genuinely unknown, while an API that hands you a narrower type still deserves its own validation.
When TypeScript Pays for Itself
TypeScript earns its keep where code has real boundaries. A boundary can be an API response, a database row, a React prop, a CLI flag, an environment variable, or a function that three different files call.
A throwaway script in plain JavaScript is fine, and that is often the right call. The arithmetic changes once a project keeps changing and other people have to touch it.
- Frontend apps with shared component props and server data.
- Full-stack apps where route handlers and UI code share models.
- Libraries and SDKs other developers consume directly.
- Backend services with request, response, and job payload contracts.
- AI-generated code that wants a compiler-backed review pass.
The catch is that types only help when they are honest. A project full of any, wide object shapes, and assertions turns the checker into decoration, and decoration does not catch bugs.
The Problems Each Topic Solves
Values That Stop Matching the Code
The first problem usually takes one of two shapes. A value arrives from outside your code and does not match what the code assumed, or an internal change such as a mutation, a refactor, or a stale declaration moves the shape out from under it.
Naming the shape is half the answer. A declaration states once what a value contains, and every function the compiler checks is measured against that statement rather than against your memory of it. An any, an assertion, or a file left out of the project slips past it.
The boundaries worth naming first are the ones carrying data your code has not verified. A fetch response, a form submission, a webhook body, and an environment variable all need checking on arrival, whoever produced them.
type User = { id: string; email: string };
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Record<string, unknown>;
return typeof candidate.id === "string"
&& typeof candidate.email === "string";
} The other half is the check. Because the type layer is erased, a declaration on its own says nothing about what the server actually sent. The guard above is ordinary JavaScript that runs, and it is what earns the type on the far side.
Unions cover the related case, where a value is legitimately several shapes rather than one. A union that models the states precisely, narrowed before use, keeps impossible combinations out of the checked code, though an any or an unvalidated payload can still get through.
The types topic covers annotations, interfaces and aliases, unions and narrowing, and runtime data across four guides.
The Same Function, Written Three Times
The second problem is duplication the compiler cannot see. You write a helper for users, copy it for orders, copy it again for invoices, and the three drift apart over the months that follow. One way that surfaces is a field renamed in one copy and missed in the others.
Widening the parameter to any removes the duplication by switching checking off for that value, which is not the same as the compiler not knowing its type. A type parameter instead carries the caller's type through the helper and keeps it.
type User = { id: string; email: string };
type Order = { id: string; total: number };
const users: User[] = [{ id: "u1", email: "ada@example.com" }];
const orders: Order[] = [{ id: "o1", total: 4200 }];
function firstItem<Item>(items: Item[]): Item | undefined {
return items[0];
}
const firstUser = firstItem(users); // User | undefined
const firstOrder = firstItem(orders); // Order | undefined One helper, and each call site keeps the type it arrived with. The compiler usually infers the parameter from the arguments, though some positions supply no candidate and you then supply it yourself.
Constraints narrow what callers may pass when a helper needs particular fields. <Item extends { id: string }> accepts any argument structurally assignable to that shape and rejects the rest in checked code. A parameter appearing in only one position is usually connecting nothing, though one used once can still capture a type for a later inference.
Utility types apply the same mechanism to shapes you already declared, so Pick<User, "email"> derives a type rather than restating one. Rename a field and the derived type turns the mismatch into a compiler error, though it cannot catch drift that any or unvalidated runtime data hides.
The generics topic covers function types, type parameters with constraints and defaults, and the built-in utility types.
Green in the Editor, Red in CI
The third problem is a tooling and configuration one rather than a modelling one. Your editor shows nothing wrong, the build succeeds, and the pipeline fails anyway, or the reverse happens and only the editor complains.
Often the two are not doing the same job. A build tool that only strips types tells you the code transpiled, not that it type-checks, while a build that invokes tsc establishes both. A separate check step closes the gap left by the first kind.
tsc --noEmitchecks without writing output, over the files the nearesttsconfig.jsonselects.- The editor's language service checks as you type, using the config it resolves for that file.
- A CI step can run the same check on every push, given the same command and config, which is where disagreements surface.
Node muddies it further by running TypeScript files directly, stripping types with no flag since 23.6, backported to 22.18 and stable since 24.12. It executes erasable syntax only and ignores tsconfig.json, so it will happily run code the compiler would reject.
The config causes much of the rest. tsconfig.json decides which files are checked, how strict the checking is, and what the compiler emits, for the invocations that load it and subject to command-line overrides.
Copying one between a library, a Node service, and a bundled app is a common source of mismatches. Strictness settings tend to travel better than module settings, and the usual recommendation is a separate config per runtime environment rather than one shared file.
React props and API contracts are two places these settings show up in application code, and a mismatched config can surface there as a type error that takes a while to trace. The tooling topic covers tsconfig.json, the transpile-versus-check split, React components, and API contracts.
Common Mistakes
Four common mistakes, each of them usually blamed on the language rather than the model underneath.
- Treating the compiler as a linter to appease: an error usually means the model is unclear, and the fix is often a better type or a simpler function rather than another annotation.
- Reaching for
asinstead of a check: an assertion is a promise, not a validation, as the runtime data guide shows. - Making every property optional: a pile of
?fields hides which combinations are legal, and discriminated unions name the states instead. - Letting a shared types file become a dumping ground: types belong near the code they describe until they are genuinely shared, a line the interfaces and aliases guide draws.
const user = JSON.parse(input) as User; That line checks nothing. It tells TypeScript to trust you, and TypeScript will do exactly that, right up until the server sends a field you did not expect.
TypeScript with AI Coding Tools
An assistant can produce typed-looking code in seconds, so name the types before you ask for the implementation. Give it the data shape, the allowed states, the runtime boundary, and the strictness setting you are working under.
Build a React settings component in TypeScript strict mode.
Use this Settings type, model loading/success/error as a discriminated union,
avoid any, and keep runtime API data behind a validation function. After generation, read the type model before you read the code. If the types are mushy, the implementation usually is too.
anywhereunknown, a union, or a named type belongs.- Optional fields standing in for states that should be modelled.
- Runtime data trusted because the assistant wrote
as SomeType. - Response types copied by hand from backend code and already drifting.
The best AI-generated TypeScript tends to have boring types, and boring is the compliment here. It means the contracts are named and the compiler can do its job.
Every Guide in This Section
The Type System
Start here if annotations are new. The four guides build on each other in order, and the types topic holds them together.
- The First Types to Learn: primitive annotations, arrays, object types, and where inference already knows the answer.
- Interfaces and Type Aliases: what each form can express, extension, declaration merging, and which one to default to.
- Union Types and Narrowing: literal types, the narrowing toolkit, discriminated unions, and exhaustiveness with
never. - unknown, any, and Runtime Data: when
unknownis the right starting type at a boundary, how to validate parsed JSON, and what an assertion really costs.
Reusable Types and Generics
These three are about naming a shape once and reusing it without copying it. The generics topic covers how they connect.
- Typing Functions and Callbacks: parameters, return types, optional and rest parameters, and the contextual typing that saves the annotation.
- Generic Functions and Types: type parameters, inference at the call site, constraints with
extends, and defaults. - Utility Types:
Pick,Omit,Partial,Record,ReturnType, and when a named type beats a clever one.
Configuration and Tooling
The half of TypeScript that is not types: the compiler, the config file, and two contexts with typing rules of their own, React components and API contracts. All four sit under the tooling topic.
- tsconfig.json Explained: which files get checked, what
strictturns on, the flags that stay off until you name them, and sharing a config. - Tooling and Project Workflow: the compiler commands, why transpiling is not type checking, a real check in CI, and migrating a JavaScript codebase.
- TypeScript with React: JSX settings, prop types, typed state and refs, event handlers, and the server and client boundary.
- API Contracts and Shared Models: modelling a request and response once, generating types from a schema, and why validation stays a separate step.
What TypeScript Cannot Do
The static checker does not validate runtime values, which is the erasure rule restated as a limit. Your program can of course run checks, type guards, and validators; TypeScript is simply not doing it for you, so network responses, form input, and environment variables need code you write.
It also cannot rescue a bad model. A type can describe a confusing domain perfectly, and the domain stays confusing; naming the states is design work the checker only enforces afterwards.
It does not replace tests either. Type checking confirms that shapes are assignable under the config you have set, and it permits some unsound operations deliberately. It says nothing about whether a function computes the right answer.
Finally, it is not a security boundary. Anything crossing the wire is untrusted regardless of how it is typed, which is why application security treats the client as hostile input.
Learning Path
Learn it in roughly the order you will use it at work.
- Get comfortable with JavaScript first: objects, arrays, modules, promises, and closures.
- Learn annotations, inference, object types, and function signatures.
- Learn unions and narrowing before you go near generics.
- Add generics and utility types once duplication actually appears.
- Read
tsconfig.jsonuntil the main strictness settings make sense. - Apply all of it to React props, API clients, and server data.
Skip advanced type gymnastics until something forces the issue. Conditional types, mapped types, and template literal types are powerful and are rarely where the first month of value lives.
Related Languages
- JavaScript: the runtime model TypeScript compiles down to and never replaces.
- JavaScript vs TypeScript: the decision itself, if you are still weighing whether types are worth the step.
- Go: a typed backend language when deployment simplicity and concurrency matter more than the browser.
- Rust: a stricter compiler still, when memory safety and low-level control are part of the job.
- C#: a mature managed platform when .NET, ASP.NET Core, and enterprise tooling are the environment.
- Programming: the wider language index, including the data and database guides.
Frequently Asked Questions
Is TypeScript replacing JavaScript?
No. TypeScript compiles to JavaScript and runs on the same engines, so it is a layer over the language rather than a successor. A browser build ships JavaScript once the types are removed, though some projects only type-check with noEmit, emit declarations alone, or run .ts files directly in Node, Deno, or Bun.
Do you need to learn JavaScript before TypeScript?
Yes, at least the fundamentals. TypeScript adds a type layer on top of JavaScript semantics, so closures, asynchronous behaviour, and the way objects and arrays actually work still apply. Learning both at once tends to hide which half is causing a problem.
Does TypeScript make code run slower?
No. The type layer is erased during compilation, so annotations and interfaces add nothing to the shipped JavaScript. Some compiler options do change the output, such as an older target that downlevels syntax or adds helpers, but the everyday cost of type checking lands at build time and in the editor.
What are .d.ts declaration files for?
They describe the shape of code without containing it, so TypeScript can check calls into plain JavaScript libraries, built-ins, and published TypeScript APIs. Many packages ship their own. For untyped packages, community definitions often live under npm's @types scope, and where none exists you write the declarations yourself.
Which section should you start with?
Types, unless you already write annotations comfortably. Generics only pays off once you are repeating the same shapes, and tooling makes more sense after strict-mode errors are readable. The at-a-glance table above routes each case.
Where to Start
Build one small thing with real state before chasing syntax trivia: a task list, a settings page, or an admin table. Give it a domain type with a nullable field, a load state modelled as a discriminated union, one API function whose response is validated at the boundary, and a component with narrow props.
That exercise touches annotations, unions, narrowing, runtime data, and props in one sitting, which is most of what the language asks you to hold in your head. Work through the first types to learn if annotations are new, then unions and narrowing.
Run tsc --noEmit from the project directory when you want the type check on its own. It reports errors in the files the nearest tsconfig.json selects and writes no output, so running it elsewhere can check the wrong project. It does not transpile, run, or test your code, which makes a clean pass necessary rather than sufficient.
Sources
-
[1]
TypeScript Handbook(typescriptlang.org)
-
[2]
Everyday Types(typescriptlang.org)
-
[3]
Narrowing(typescriptlang.org)
-
[4]
Generics(typescriptlang.org)
-
[5]
Utility Types(typescriptlang.org)
-
[6]
TSConfig Reference(typescriptlang.org)
-
[7]
TypeScript 5.9 Release Notes(typescriptlang.org)
Read Next
The map of the JavaScript section: what the language does, the single-thread rule every guide assumes, why old advice looks wrong, and where all thirteen guides across four topics fit.
JavaScript and TypeScript are the same language with one big difference: types. Here is what TypeScript actually adds, when the extra step pays off, and which one a beginner should start with.
A starting point for browser-side programming on CodeWalkers: the languages that run in the browser, and the frameworks built on top of them.