TypeScript is JavaScript with a type system bolted on top. Developed by Microsoft and first released in 2012, it compiles to plain JavaScript and runs anywhere JavaScript runs — browsers, Node.js, Deno, Bun, serverless functions. By 2026, TypeScript has become the default language for professional web development, and the question is no longer whether to use it but how to use it well.
This guide walks through the TypeScript type system from fundamentals to advanced patterns, with practical examples you can apply to real projects immediately.
Why TypeScript Exists
JavaScript was designed in ten days for adding interactivity to web pages. It was never intended to power applications with hundreds of thousands of lines of code, teams of dozens of developers, and deployment targets ranging from browsers to servers to edge functions. TypeScript fills that gap by adding static types that catch errors at compile time rather than at runtime.
Consider this JavaScript function:
function calculatePrice(items, taxRate) {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0) * (1 + taxRate);
}
Without types, nothing prevents you from calling calculatePrice("hello", true). The function will silently produce NaN instead of throwing an error. TypeScript prevents this entire category of bugs:
interface CartItem {
name: string;
price: number;
quantity: number;
}
function calculatePrice(items: CartItem[], taxRate: number): number {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0) * (1 + taxRate);
}
// Compile error: Argument of type 'string' is not assignable
calculatePrice("hello", true); // ✗ caught at compile time
Setting Up TypeScript
Getting started with TypeScript requires minimal setup. Most modern frameworks include TypeScript configuration out of the box, but understanding the basics helps when you need to customize behavior.
# Install TypeScript
npm install -D typescript
# Initialize a tsconfig.json
npx tsc --init
# Compile TypeScript files
npx tsc
# Watch mode for development
npx tsc --watch
A solid tsconfig.json for new projects in 2026 enables strict checking from the start:
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Core Type System
TypeScript’s type system is structural, not nominal. This means types are compatible if their shapes match, regardless of how they are named. Understanding this principle explains most of TypeScript’s behavior.
Primitive Types and Type Annotations
// Primitive types
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// Arrays
let scores: number[] = [95, 87, 92];
let names: Array<string> = ["Alice", "Bob"];
// Tuples — fixed-length arrays with specific types per position
let coordinate: [number, number] = [40.7128, -74.0060];
let record: [string, number, boolean] = ["Alice", 30, true];
// Enums — named constants
enum Status {
Pending = "pending",
Active = "active",
Archived = "archived"
}
let userStatus: Status = Status.Active;
Interfaces and Type Aliases
Interfaces define the shape of objects. Type aliases can do the same but also handle unions, intersections, and mapped types. In practice, use interfaces for object shapes and type aliases for everything else.
// Interface — describes an object shape
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
preferences?: UserPreferences; // optional property
}
interface UserPreferences {
theme: "light" | "dark" | "system";
language: string;
notifications: boolean;
}
// Extending interfaces
interface AdminUser extends User {
role: "admin";
permissions: string[];
lastAuditAt: Date;
}
// Type alias — works for unions, intersections, primitives
type ID = string | number;
type Result<T> = { success: true; data: T } | { success: false; error: string };
type EventHandler = (event: MouseEvent) => void;
Union Types and Narrowing
Union types let a value be one of several types. TypeScript’s control flow analysis narrows the type based on runtime checks, which is one of its most powerful features.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
// TypeScript knows the exact type in each branch
const area = calculateArea({ kind: "circle", radius: 5 }); // 78.54
This pattern — called discriminated unions — replaces class hierarchies and complex inheritance chains with simple, type-safe data structures. It works naturally with React state management and API response handling.
Generics
Generics let you write functions, interfaces, and classes that work with any type while preserving type information. They are the key to writing reusable, type-safe code.
// A generic function that works with any type
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number | undefined
const str = first(["a", "b", "c"]); // type: string | undefined
// Generic with constraints
interface HasId {
id: string | number;
}
function findById<T extends HasId>(items: T[], id: T["id"]): T | undefined {
return items.find(item => item.id === id);
}
// Generic interface for API responses
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
pagination?: {
page: number;
totalPages: number;
totalItems: number;
};
}
// Usage preserves the specific data type
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const response = await fetch("/api/users");
return response.json();
}
const result = await fetchUsers();
result.data[0].name; // TypeScript knows this is a string
Utility Types
TypeScript includes built-in utility types that transform existing types without rewriting them. These save significant time when working with complex type structures.
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Partial — makes all properties optional
type UserUpdate = Partial<User>;
// Pick — select specific properties
type UserPublicProfile = Pick<User, "id" | "name" | "role">;
// Omit — exclude specific properties
type UserCreateInput = Omit<User, "id" | "createdAt">;
// Required — makes all properties required
type CompleteUser = Required<User>;
// Readonly — prevents mutation
type FrozenUser = Readonly<User>;
// Record — create object types with specific key and value types
type UserRoles = Record<string, "admin" | "editor" | "viewer">;
// ReturnType — extract return type from a function
function createUser(input: UserCreateInput) {
return { ...input, id: Date.now(), createdAt: new Date() };
}
type CreatedUser = ReturnType<typeof createUser>;
Advanced Patterns
Template Literal Types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type EventName = `on${Capitalize<string>}`;
// Practical example: typed event emitter
type EventMap = {
click: { x: number; y: number };
keypress: { key: string; code: number };
scroll: { offsetY: number };
};
function on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void
): void {
// implementation
}
on("click", (payload) => {
console.log(payload.x, payload.y); // fully typed
});
Conditional Types
// Type that changes based on a condition
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<number>; // false
// Extract non-nullable types
type NonNullable<T> = T extends null | undefined ? never : T;
// Infer keyword — extract types from other types
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type X = UnpackPromise<Promise<string>>; // string
type Y = UnpackPromise<number>; // number
TypeScript with React
React and TypeScript work together naturally. Component props, state, event handlers, and refs all benefit from type checking. Most modern React setups include TypeScript by default.
import { useState, useCallback } from 'react';
interface TodoItem {
id: string;
text: string;
completed: boolean;
createdAt: Date;
}
interface TodoListProps {
initialItems?: TodoItem[];
onItemComplete?: (id: string) => void;
maxItems?: number;
}
function TodoList({ initialItems = [], onItemComplete, maxItems = 50 }: TodoListProps) {
const [items, setItems] = useState<TodoItem[]>(initialItems);
const [input, setInput] = useState("");
const addItem = useCallback(() => {
if (!input.trim() || items.length >= maxItems) return;
const newItem: TodoItem = {
id: crypto.randomUUID(),
text: input.trim(),
completed: false,
createdAt: new Date()
};
setItems(prev => [...prev, newItem]);
setInput("");
}, [input, items.length, maxItems]);
const toggleItem = useCallback((id: string) => {
setItems(prev =>
prev.map(item =>
item.id === id ? { ...item, completed: !item.completed } : item
)
);
onItemComplete?.(id);
}, [onItemComplete]);
return (
<div>
<input
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && addItem()}
/>
{items.map(item => (
<div key={item.id} onClick={() => toggleItem(item.id)}>
{item.text}
</div>
))}
</div>
);
}
Migration Strategy: JavaScript to TypeScript
Migrating an existing JavaScript project does not require rewriting everything at once. TypeScript supports gradual adoption through several mechanisms.
Step 1: Add TypeScript With Loose Settings
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false,
"noEmit": true
},
"include": ["src/**/*"]
}
Step 2: Rename Files Incrementally
Rename .js files to .ts one at a time, starting with utility functions and data models that other modules depend on. TypeScript accepts valid JavaScript, so renamed files compile without changes initially. For frontend projects with responsive layouts and complex component trees, start by typing the shared components and utility functions first.
Step 3: Enable Strict Checks Gradually
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
Enable one strict flag at a time, fix the resulting errors, and then move to the next. The order above reflects increasing difficulty — noImplicitAny typically produces the most errors but is the most valuable to fix first.
Step 4: Use JSDoc for Files That Stay as JavaScript
/**
* @param {string} name
* @param {{ role: "admin" | "user"; age: number }} options
* @returns {Promise<User>}
*/
async function createUser(name, options) {
// TypeScript checks this function using JSDoc types
// without requiring a .ts file extension
}
TypeScript Tooling Ecosystem
The TypeScript ecosystem extends beyond the compiler. VS Code provides the richest TypeScript editing experience with inline error display, automatic imports, rename refactoring, and go-to-definition that works across module boundaries. ESLint with typescript-eslint catches additional patterns that the compiler misses, such as unused variables and consistent type import styles. Prettier handles formatting so teams avoid style debates entirely.
For performance-sensitive projects, tools like tsx (for running TypeScript directly in Node.js) and tsup (for bundling TypeScript libraries) simplify the build pipeline. The trend in 2026 is toward tools that strip types at build time without full type checking, using the TypeScript compiler only for validation in CI.
Common Mistakes to Avoid
Several patterns that seem reasonable lead to problems in TypeScript codebases:
- Overusing
any— Everyanytype disables type checking for everything it touches. Useunknowninstead and narrow with type guards - Type assertions instead of type guards —
value as Usertells the compiler to trust you without verification. A type guard (if ('name' in value)) actually checks the data at runtime - Ignoring strict null checks — Optional chaining (
user?.name) and nullish coalescing (value ?? default) handle null safely. Disabling strict null checks hides real bugs - Duplicating types instead of deriving them — Use utility types (
Pick,Omit,Partial) to derive types from a single source of truth - Not using discriminated unions — When modeling data with multiple variants (API responses, state machines), discriminated unions provide exhaustive checking that catches missing cases at compile time
Frequently Asked Questions
Does TypeScript make my code slower?
No. TypeScript compiles to plain JavaScript, and all type information is erased during compilation. The runtime code is identical to what you would write in JavaScript. The compilation step adds a few seconds to your build process, but the resulting JavaScript runs at the same speed.
Can I use TypeScript with Vue or Svelte?
Yes. Vue 3 was rewritten in TypeScript and has first-class type support through the Composition API and <script setup lang="ts">. Svelte has native TypeScript support in <script lang="ts"> blocks. Both frameworks provide typed component props, events, and slots. See our framework comparison for details on TypeScript support across frameworks.
How long does a typical JavaScript-to-TypeScript migration take?
For a medium-sized project (50-100 files), expect two to four weeks of part-time effort using the gradual migration strategy. The biggest time investment is adding types to API boundaries and shared utilities. Internal implementation details can use inferred types and need fewer explicit annotations. Most teams report that the migration pays for itself within the first month through reduced debugging time.
Should I use interfaces or type aliases?
Use interfaces for object shapes that might be extended (component props, API responses, data models). Use type aliases for unions, intersections, mapped types, and primitives. In practice, either works for most cases — the important thing is to be consistent within a project. Many teams default to interfaces for objects and type aliases for everything else.