TypeScript is JavaScript with types — and those types catch bugs before your code ever runs. This tutorial takes you from zero TypeScript knowledge to writing type-safe applications, with practical examples at every step.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Compile and run TypeScript files |
| Basic types | Annotate variables, parameters, return values |
| Interfaces & types | Define shapes for objects and data |
| Functions | Write typed, overloaded functions |
| Classes | Use OOP with full type safety |
| Generics | Write reusable, flexible typed code |
| Utility types | Use built-in TypeScript helpers |
| Modules | Organise code into typed modules |
| Projects | Build 3 real TypeScript programs |
TypeScript version: 5.x (2025, all modern features)
Part 1 — Why TypeScript?
| Feature | JavaScript | TypeScript |
|---|---|---|
| Type checking | Runtime errors | Compile-time errors |
| IDE support | Basic autocomplete | Full IntelliSense |
| Refactoring | Manual, risky | Safe, automated |
| Documentation | Comments only | Types are docs |
| Team collaboration | Easy to break | Contracts enforced |
| Learning curve | None extra | Small upfront cost |
The core promise
// JavaScript — this crashes at runtime
function greet(user) {
return "Hello, " + user.nme; // typo: nme instead of name
}
greet({ name: "Alice" }); // "Hello, undefined" — no error!
// TypeScript — this fails at compile time
function greet(user: { name: string }): string {
return "Hello, " + user.nme; // TS error: Property 'nme' does not exist
}
TypeScript catches this instantly, before you even run the code.
TypeScript vs JavaScript vs other typed languages
| Language | Typing | Runs where | Best for |
|---|---|---|---|
| JavaScript | Dynamic | Browser + Node.js | Web, quick scripts |
| TypeScript | Static (optional) | Browser + Node.js | Large web apps, teams |
| Java | Static, strict | JVM | Enterprise backend |
| C# | Static, strict | .NET | Windows apps, game dev |
| Python | Dynamic (type hints) | Everywhere | Data science, scripting |
| Go | Static | Everywhere | System tools, APIs |
TypeScript gives you JavaScript's ecosystem with the safety of a statically typed language.
Part 2 — Setup
Install TypeScript
# Install globally
npm install -g typescript
# Verify
tsc --version # TypeScript 5.x.x
Your first TypeScript file
Create hello.ts:
const message: string = "Hello, TypeScript!";
console.log(message);
Compile and run:
tsc hello.ts # produces hello.js
node hello.js # Hello, TypeScript!
Project setup with tsconfig.json
For real projects, create a config file:
tsc --init
tsconfig.json essentials:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Compile and watch
tsc # compile once
tsc --watch # recompile on file changes
Run TypeScript directly (development)
npm install -g ts-node
ts-node src/index.ts
Or with tsx (faster):
npm install -g tsx
tsx src/index.ts
Part 3 — Basic Types
Primitive types
// String
let name: string = "Alice";
let greeting: string = `Hello, ${name}!`;
// Number (all numbers: integer + float)
let age: number = 30;
let price: number = 9.99;
let hex: number = 0xff;
// Boolean
let isActive: boolean = true;
let hasAccount: boolean = false;
// BigInt
let bigNum: bigint = 9007199254740991n;
// Symbol
let id: symbol = Symbol("id");
Special types
// null and undefined
let nothing: null = null;
let missing: undefined = undefined;
// any — escape hatch (avoid when possible)
let anything: any = 42;
anything = "now a string"; // no error, no type checking
// unknown — safer than any
let userInput: unknown = getInput();
if (typeof userInput === "string") {
console.log(userInput.toUpperCase()); // safe after narrowing
}
// never — a function that never returns
function throwError(message: string): never {
throw new Error(message);
}
// void — function with no return value
function logMessage(msg: string): void {
console.log(msg);
}
Type inference
TypeScript often infers types automatically:
let count = 0; // inferred: number
let label = "error"; // inferred: string
let active = true; // inferred: boolean
count = "hello"; // Error: Type 'string' is not assignable to type 'number'
Rule: Let TypeScript infer when obvious. Annotate when the type isn't clear.
Type annotation quick reference
| Type | Example | Notes |
|---|---|---|
string |
let s: string = "hi" |
Text |
number |
let n: number = 42 |
All numbers |
boolean |
let b: boolean = true |
true/false |
null |
let n: null = null |
Explicit null |
undefined |
let u: undefined |
Not yet set |
any |
let a: any |
Opt-out of checking |
unknown |
let u: unknown |
Safe any |
never |
Return type | Never returns |
void |
Return type | No return value |
object |
let o: object |
Non-primitive |
Part 4 — Arrays and Tuples
Arrays
// Two equivalent syntaxes
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
// Array of objects
let users: { name: string; age: number }[] = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
// Readonly array — prevents mutation
const ids: readonly number[] = [1, 2, 3];
ids.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'
Tuples
A tuple is a fixed-length array where each position has a specific type:
// Tuple: [string, number]
let person: [string, number] = ["Alice", 30];
let name = person[0]; // string
let age = person[1]; // number
// Labeled tuple (TypeScript 4.0+)
type Point = [x: number, y: number, z?: number];
const p: Point = [10, 20]; // z is optional
const p3d: Point = [10, 20, 30]; // all three
// Destructuring a tuple
const [firstName, userAge]: [string, number] = ["Bob", 25];
Part 5 — Interfaces and Type Aliases
These are the two main ways to define shapes in TypeScript.
Interface
interface User {
id: number;
name: string;
email: string;
age?: number; // optional
readonly role: string; // cannot be changed after creation
}
function getUser(id: number): User {
return { id, name: "Alice", email: "alice@example.com", role: "admin" };
}
const user = getUser(1);
user.role = "user"; // Error: Cannot assign to 'role' because it is a read-only property
Type alias
type Point = {
x: number;
y: number;
};
type ID = string | number; // union type
type Status = "active" | "inactive" | "pending"; // literal union
Interface vs type alias
| Feature | Interface | Type alias |
|---|---|---|
| Object shapes | Yes | Yes |
| Union types | No | Yes |
Intersection (&) |
Via extends |
Yes |
| Extending | extends keyword |
& operator |
| Declaration merging | Yes (can reopen) | No |
| Best for | Objects, classes | Unions, primitives, utilities |
// Extending an interface
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Extending a type alias
type Animal2 = { name: string };
type Dog2 = Animal2 & { breed: string };
// Declaration merging (interface only)
interface Window {
myCustomProp: string; // adds to the existing Window interface
}
Rule of thumb: Use interface for objects/classes. Use type for unions, intersections, and mapped types.
Part 6 — Union and Intersection Types
Union types
A value can be one of several types:
type ID = string | number;
function printId(id: ID): void {
if (typeof id === "string") {
console.log(id.toUpperCase()); // string methods available
} else {
console.log(id.toFixed(0)); // number methods available
}
}
printId("abc123"); // works
printId(42); // works
Discriminated unions
The pattern for modelling state machines:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
Intersection types
Combine multiple types into one:
type Timestamped = { createdAt: Date; updatedAt: Date };
type Identifiable = { id: string };
type Entity = Timestamped & Identifiable;
const post: Entity = {
id: "post-1",
createdAt: new Date(),
updatedAt: new Date(),
};
Part 7 — Functions
Function type annotations
// Parameter and return type
function add(a: number, b: number): number {
return a + b;
}
// Arrow function
const multiply = (a: number, b: number): number => a * b;
// Optional parameter
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}!`;
}
// Default parameter
function createUser(name: string, role: string = "user"): object {
return { name, role };
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
sum(1, 2, 3, 4); // 10
Function types
// Function type alias
type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;
const sub: MathOp = (a, b) => a - b;
// Function as parameter
function applyOp(a: number, b: number, op: MathOp): number {
return op(a, b);
}
applyOp(10, 3, add); // 13
applyOp(10, 3, sub); // 7
Overloads
Different signatures for different inputs:
function formatInput(input: string): string;
function formatInput(input: number): string;
function formatInput(input: string | number): string {
if (typeof input === "string") {
return input.trim().toLowerCase();
}
return input.toFixed(2);
}
formatInput(" Hello "); // "hello"
formatInput(3.14159); // "3.14"
Part 8 — Classes
TypeScript adds access modifiers and other features to JavaScript classes.
Basic class
class Animal {
// Class fields with types
name: string;
private age: number; // only accessible inside this class
protected species: string; // accessible in subclasses
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this.species = species;
}
speak(): string {
return `${this.name} makes a sound.`;
}
getAge(): number {
return this.age; // accessible here
}
}
const cat = new Animal("Whiskers", 3, "Felis catus");
cat.name; // "Whiskers"
cat.age; // Error: Property 'age' is private
cat.getAge(); // 3
Constructor shorthand
// Declare and assign in one step
class User {
constructor(
public name: string,
private email: string,
readonly id: number
) {}
getEmail(): string {
return this.email;
}
}
const user = new User("Alice", "alice@example.com", 1);
user.name; // "Alice"
user.email; // Error: private
user.id; // 1
user.id = 2; // Error: readonly
Inheritance
class Dog extends Animal {
breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age, "Canis lupus familiaris");
this.breed = breed;
}
speak(): string {
return `${this.name} barks!`; // override parent method
}
describe(): string {
// can access protected species from parent
return `${this.name} is a ${this.breed} (${this.species})`;
}
}
const rex = new Dog("Rex", 2, "Labrador");
rex.speak(); // "Rex barks!"
rex.describe(); // "Rex is a Labrador (Canis lupus familiaris)"
Abstract classes
abstract class Shape {
abstract area(): number; // must be implemented by subclasses
describe(): string {
return `This shape has area: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle(5);
c.area(); // 78.54
c.describe(); // "This shape has area: 78.54"
Implementing interfaces
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
interface Loggable {
log(): void;
}
class DataStore implements Serializable, Loggable {
private data: Record<string, unknown> = {};
serialize(): string {
return JSON.stringify(this.data);
}
deserialize(data: string): void {
this.data = JSON.parse(data);
}
log(): void {
console.log(this.data);
}
}
Access modifier summary
| Modifier | Class | Subclass | Outside |
|---|---|---|---|
public (default) |
Yes | Yes | Yes |
protected |
Yes | Yes | No |
private |
Yes | No | No |
readonly |
Read only | Read only | Read only |
Part 9 — Generics
Generics let you write flexible, reusable code that works with any type.
Generic functions
// Without generics — loses type info
function identity(arg: any): any {
return arg;
}
// With generics — type is preserved
function identity<T>(arg: T): T {
return arg;
}
identity<string>("hello"); // type: string
identity<number>(42); // type: number
identity("inferred"); // type inferred as string
Generic with constraints
// T must have a length property
function logLength<T extends { length: number }>(arg: T): T {
console.log(`Length: ${arg.length}`);
return arg;
}
logLength("hello"); // Length: 5
logLength([1, 2, 3]); // Length: 3
logLength(42); // Error: number doesn't have length
Generic interfaces and classes
interface Repository<T> {
findById(id: number): T | undefined;
findAll(): T[];
save(item: T): void;
delete(id: number): void;
}
class InMemoryRepo<T extends { id: number }> implements Repository<T> {
private items: T[] = [];
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
findAll(): T[] {
return [...this.items];
}
save(item: T): void {
const index = this.items.findIndex(i => i.id === item.id);
if (index >= 0) {
this.items[index] = item;
} else {
this.items.push(item);
}
}
delete(id: number): void {
this.items = this.items.filter(i => i.id !== id);
}
}
// Usage
interface Product { id: number; name: string; price: number }
const productRepo = new InMemoryRepo<Product>();
productRepo.save({ id: 1, name: "Widget", price: 9.99 });
productRepo.findById(1); // { id: 1, name: "Widget", price: 9.99 }
Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const result = pair("age", 30); // [string, number]
Part 10 — Utility Types
TypeScript has built-in utility types for common transformations.
Most useful utility types
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
}
// Partial — all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; ... }
// Required — all properties required
type RequiredUser = Required<PartialUser>;
// same as User
// Readonly — all properties readonly
type ReadonlyUser = Readonly<User>;
// cannot modify any property
// Pick — select some properties
type PublicUser = Pick<User, "id" | "name" | "role">;
// { id: number; name: string; role: "admin" | "user" }
// Omit — remove some properties
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; role: "admin" | "user" }
// Record — dictionary type
type UserMap = Record<string, User>;
const users: UserMap = {
"user-1": { id: 1, name: "Alice", email: "a@a.com", password: "hash", role: "user" },
};
// Exclude — remove from union
type AdminOrUser = "admin" | "user" | "guest";
type RegularRole = Exclude<AdminOrUser, "admin">; // "user" | "guest"
// Extract — keep matching types from union
type AdminOnly = Extract<AdminOrUser, "admin">; // "admin"
// NonNullable — remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// ReturnType — extract function return type
function getUser(): User { /* ... */ return {} as User; }
type UserReturn = ReturnType<typeof getUser>; // User
// Parameters — extract function parameter types
type GetUserParams = Parameters<typeof getUser>; // []
// Awaited — unwrap Promise
type ApiResponse = Promise<{ data: User[] }>;
type Resolved = Awaited<ApiResponse>; // { data: User[] }
Utility types quick reference
| Utility | Description | Example |
|---|---|---|
Partial<T> |
All props optional | Form state |
Required<T> |
All props required | Validated object |
Readonly<T> |
No mutation | Config objects |
Pick<T, K> |
Select keys | DTO / API response |
Omit<T, K> |
Remove keys | Hide sensitive fields |
Record<K, V> |
Dictionary | Lookup maps |
Exclude<T, U> |
Remove from union | Filtering roles |
Extract<T, U> |
Keep matching | Filtering roles |
NonNullable<T> |
Remove null/undefined | After validation |
ReturnType<F> |
Function return type | Infer from function |
Parameters<F> |
Function params | Proxy functions |
Awaited<T> |
Unwrap Promise | Async return types |
Part 11 — Type Narrowing
TypeScript narrows types based on runtime checks.
type Input = string | number | null;
function process(input: Input): string {
// typeof guard
if (typeof input === "string") {
return input.toUpperCase(); // string methods available
}
// null check
if (input === null) {
return "no input";
}
// After ruling out string and null, input must be number
return input.toFixed(2);
}
// instanceof guard
function formatDate(value: string | Date): string {
if (value instanceof Date) {
return value.toISOString();
}
return new Date(value).toISOString();
}
// in operator — checks if property exists
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(animal: Cat | Dog): void {
if ("meow" in animal) {
animal.meow(); // Cat
} else {
animal.bark(); // Dog
}
}
// Type predicates — custom type guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process2(value: unknown): void {
if (isString(value)) {
console.log(value.toUpperCase()); // TypeScript knows it's string
}
}
Part 12 — Enums
// Numeric enum (default, starts at 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// String enum (more readable, preferred)
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
function processStatus(status: Status): string {
switch (status) {
case Status.Active: return "Processing...";
case Status.Inactive: return "Skipped.";
case Status.Pending: return "Waiting...";
}
}
processStatus(Status.Active); // "Processing..."
// Const enum — inlined at compile time (no runtime object)
const enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
// Alternative: literal union (often preferred over enums)
type Direction2 = "up" | "down" | "left" | "right";
Part 13 — Modules and Declaration Files
Module exports and imports
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
export interface Vector2D {
x: number;
y: number;
}
// Default export
export default class Calculator {
add(a: number, b: number) { return a + b; }
}
// main.ts
import Calculator, { add, PI, Vector2D } from "./math";
const calc = new Calculator();
const sum = add(3, 4);
const point: Vector2D = { x: 1, y: 2 };
Type-only imports
// Import only the type, not the value — better for tree-shaking
import type { Vector2D } from "./math";
type Points = Vector2D[];
Declaration files (.d.ts)
When using a JavaScript library without types:
# Most popular libraries have types in DefinitelyTyped
npm install --save-dev @types/lodash
npm install --save-dev @types/node
If no types exist, create a declaration file:
// types/my-library.d.ts
declare module "my-library" {
export function doSomething(input: string): number;
export const version: string;
}
Part 14 — Advanced Types
Mapped types
// Make all properties optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Extract read-only version
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // remove readonly
};
Template literal types
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSValue = `${number}px` | `${number}em` | `${number}rem`;
const margin: CSSValue = "16px"; // valid
const bad: CSSValue = "16"; // Error
Conditional types
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<string[]>; // true
type B = IsArray<string>; // false
// Infer inside conditional types
type UnpackArray<T> = T extends (infer Item)[] ? Item : T;
type C = UnpackArray<string[]>; // string
type D = UnpackArray<number>; // number
Part 15 — Three Projects
Project 1: Type-safe Todo CLI
// todo-cli.ts
interface Todo {
id: number;
text: string;
done: boolean;
createdAt: Date;
}
type CreateTodoInput = Omit<Todo, "id" | "createdAt">;
class TodoList {
private todos: Todo[] = [];
private nextId = 1;
add(input: CreateTodoInput): Todo {
const todo: Todo = {
id: this.nextId++,
text: input.text,
done: input.done,
createdAt: new Date(),
};
this.todos.push(todo);
return todo;
}
complete(id: number): boolean {
const todo = this.todos.find(t => t.id === id);
if (!todo) return false;
todo.done = true;
return true;
}
list(filter?: "all" | "done" | "pending"): Todo[] {
switch (filter) {
case "done": return this.todos.filter(t => t.done);
case "pending": return this.todos.filter(t => !t.done);
default: return [...this.todos];
}
}
remove(id: number): boolean {
const before = this.todos.length;
this.todos = this.todos.filter(t => t.id !== id);
return this.todos.length < before;
}
}
// Usage
const list = new TodoList();
list.add({ text: "Learn TypeScript", done: false });
list.add({ text: "Build a project", done: false });
list.complete(1);
console.log(list.list("done")); // [ { id: 1, text: "Learn TypeScript", ... } ]
Project 2: Type-safe REST API client
// api-client.ts
interface ApiConfig {
baseUrl: string;
defaultHeaders?: Record<string, string>;
}
interface ApiResponse<T> {
data: T | null;
error: string | null;
status: number;
}
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
class ApiClient {
constructor(private config: ApiConfig) {}
private async request<T>(
method: "GET" | "POST" | "PUT" | "DELETE",
path: string,
body?: unknown
): Promise<ApiResponse<T>> {
const url = `${this.config.baseUrl}${path}`;
const headers: Record<string, string> = {
"Content-Type": "application/json",
...this.config.defaultHeaders,
};
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
return {
data: null,
error: `HTTP ${response.status}: ${response.statusText}`,
status: response.status,
};
}
const data: T = await response.json();
return { data, error: null, status: response.status };
} catch (err) {
return {
data: null,
error: err instanceof Error ? err.message : "Unknown error",
status: 0,
};
}
}
get<T>(path: string): Promise<ApiResponse<T>> {
return this.request<T>("GET", path);
}
post<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
return this.request<T>("POST", path, body);
}
put<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
return this.request<T>("PUT", path, body);
}
delete<T>(path: string): Promise<ApiResponse<T>> {
return this.request<T>("DELETE", path);
}
}
// Usage
const client = new ApiClient({
baseUrl: "https://jsonplaceholder.typicode.com",
});
async function main(): Promise<void> {
const { data, error } = await client.get<Post[]>("/posts");
if (error) {
console.error("Failed:", error);
return;
}
if (data) {
data.slice(0, 3).forEach(post => {
console.log(`${post.id}: ${post.title}`);
});
}
}
main();
Project 3: Generic event emitter
// event-emitter.ts
type EventMap = Record<string, unknown[]>;
type EventListener<Args extends unknown[]> = (...args: Args) => void;
class TypedEventEmitter<Events extends EventMap> {
private listeners: {
[K in keyof Events]?: EventListener<Events[K]>[];
} = {};
on<K extends keyof Events>(
event: K,
listener: EventListener<Events[K]>
): this {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
return this;
}
off<K extends keyof Events>(
event: K,
listener: EventListener<Events[K]>
): this {
const list = this.listeners[event];
if (list) {
this.listeners[event] = list.filter(l => l !== listener) as typeof list;
}
return this;
}
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
this.listeners[event]?.forEach(listener => listener(...args));
}
once<K extends keyof Events>(
event: K,
listener: EventListener<Events[K]>
): this {
const wrapper: EventListener<Events[K]> = (...args) => {
listener(...args);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}
// Define your event types
type AppEvents = {
login: [userId: string, timestamp: Date];
logout: [userId: string];
error: [message: string, code: number];
};
// Usage — fully type-safe
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on("login", (userId, timestamp) => {
// userId: string, timestamp: Date — fully typed!
console.log(`${userId} logged in at ${timestamp.toISOString()}`);
});
emitter.on("error", (message, code) => {
// message: string, code: number — fully typed!
console.error(`Error ${code}: ${message}`);
});
emitter.emit("login", "user-123", new Date());
emitter.emit("error", "Not found", 404);
// emitter.emit("login", 123, new Date()); // Error: number not assignable to string
Migrating from JavaScript to TypeScript
Step-by-step approach
- Rename
.jsfiles to.ts— fix errors one by one - Add
tsconfig.jsonwith"strict": falseinitially - Enable strict mode gradually — turn on one flag at a time
- Install type packages —
npm install -D @types/node @types/reactetc. - Replace
anywith real types progressively
Common migration patterns
// JS: implicit any
function processData(data) {
return data.map(item => item.value);
}
// TS: explicit types
function processData(data: Array<{ value: number }>): number[] {
return data.map(item => item.value);
}
// JS: object without shape
const config = {
host: "localhost",
port: 3000,
};
// TS: typed config
interface ServerConfig {
host: string;
port: number;
ssl?: boolean;
}
const config: ServerConfig = {
host: "localhost",
port: 3000,
};
TypeScript with Popular Frameworks
| Framework | How to start | Notes |
|---|---|---|
| React | npm create vite -- --template react-ts |
TSX + typed props |
| Next.js | npx create-next-app --typescript |
Full-stack TS |
| Vue 3 | npm create vue (select TS) |
<script setup lang="ts"> |
| Express | npm i express @types/express |
Typed middleware |
| NestJS | nest new project |
TypeScript-first framework |
| Prisma | npm install prisma |
Auto-generates types from schema |
React + TypeScript quick example
// Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
export function Button({
label,
onClick,
variant = "primary",
disabled = false,
}: ButtonProps): JSX.Element {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}
// Usage — TypeScript enforces all prop types
<Button
label="Submit"
onClick={() => console.log("clicked")}
variant="primary"
/>
<Button label="Delete" onClick={handleDelete} variant="danger" />
Learning Path
| Stage | Duration | What to focus on |
|---|---|---|
| 1. Basics | 1 week | Types, interfaces, functions, classes |
| 2. Intermediate | 2 weeks | Generics, utility types, narrowing |
| 3. Advanced | 2 weeks | Mapped types, conditional types, decorators |
| 4. Framework | 2–4 weeks | React/Next.js or Node.js/Express with TS |
| 5. Real project | Ongoing | Build something, read open-source TS code |
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Using any everywhere |
Defeats the purpose of TypeScript | Use unknown + narrowing |
Type assertions (as) too early |
Bypasses checking | Add proper type guard instead |
| Ignoring compiler errors | Bugs reach production | Fix errors, don't use // @ts-ignore |
interface for everything |
Some cases need type |
Use type for unions |
enum overuse |
Larger bundle, runtime object | Use const enum or literal unions |
Not enabling strict mode |
Misses many checks | Set "strict": true in tsconfig |
| Typing everything manually | Verbose, error-prone | Let TypeScript infer where it can |
Ignoring @types/ packages |
Runtime crashes with JS libs | Always install @types/ for JS libraries |
TypeScript vs Related Terms
| Term | What it is |
|---|---|
| TypeScript | JavaScript superset with static types |
| JavaScript | Dynamic language TypeScript compiles to |
| TSC | TypeScript compiler (tsc CLI) |
| tsconfig.json | TypeScript project config file |
| .d.ts file | Type declaration file (no runtime code) |
| DefinitelyTyped | Community @types/ packages repository |
| ESLint + typescript-eslint | Linting for TypeScript code |
| Babel | JS transpiler (can strip TS types, no checking) |
| SWC | Fast Rust-based TS/JS transpiler |
| Zod | Runtime schema validation that matches TS types |
Frequently Asked Questions
Do I need to know JavaScript before learning TypeScript?
Yes. TypeScript is a superset of JavaScript — every JS concept (closures, promises, prototype chain) still applies. Learn JS fundamentals first, then TypeScript becomes a natural addition.
Is TypeScript required for React/Node.js?
No, but it's strongly recommended. Most new React and Node.js projects in 2025 use TypeScript. Job listings increasingly list TS as a requirement.
Does TypeScript slow down development?
Initially, yes. Setting up types takes extra time upfront. Long-term, TypeScript speeds development through better autocomplete, safer refactoring, and fewer runtime bugs.
What's strict mode in TypeScript?
Enabling "strict": true in tsconfig turns on several strict checks: noImplicitAny, strictNullChecks, strictFunctionTypes, and others. Always enable it for new projects.
What's the difference between TypeScript and JavaScript at runtime?
There is no TypeScript at runtime — the compiler strips all types and produces plain JavaScript. TypeScript only exists at development and compile time.
Should I use interface or type?
Use interface for defining object shapes that might be extended or implemented by classes. Use type for unions, intersections, mapped types, and utility transformations. When in doubt, either works.
Quick Reference
// Variables
let name: string = "Alice";
let count: number = 0;
let active: boolean = true;
let anything: unknown = getValue();
// Arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// Tuple
let pair: [string, number] = ["age", 30];
// Interface
interface User { id: number; name: string; age?: number }
// Type alias
type Status = "active" | "inactive";
// Generic function
function wrap<T>(value: T): T[] { return [value]; }
// Utility types
type P = Partial<User>; // all optional
type S = Pick<User, "id">; // select keys
type O = Omit<User, "age">; // remove keys
// Union
function handle(input: string | number): void { /* ... */ }
// Intersection
type Admin = User & { permissions: string[] };
// Type narrowing
if (typeof value === "string") { /* value is string */ }
if (value instanceof Date) { /* value is Date */ }
// Optional chaining + nullish coalescing
const city = user?.address?.city ?? "Unknown";
// Non-null assertion (use sparingly)
const el = document.getElementById("app")!;
// Type assertion (use sparingly)
const input = event.target as HTMLInputElement;