TypeScript is a strict syntactical superset of JavaScript developed by Anders Hejlsberg at Microsoft (released 2012). Every valid JS file is valid TS. TypeScript compiles (tsc) to plain JavaScript — types are erased at runtime.
The key insight: JavaScript's dynamic typing becomes a liability at scale. TypeScript adds a static type system that catches errors at compile time, enables powerful IDE tooling, and serves as living documentation.
# Install
npm install -D typescript
# Compile
npx tsc index.ts # → index.js
npx tsc --watch # recompile on change
npx tsc --init # generate tsconfig.json
// Primitives
let name: string = "Alice";
let age: number = 30;
let active: boolean = true;
let data: null = null;
let val: undefined = undefined;
// Arrays & tuples
let ids: number[] = [1, 2, 3];
let pair: [string, number] = ["age", 30];
// Object type
let user: { name: string; age: number } = {
name: "Alice",
age: 30,
};
// TS infers types automatically
let city = "London"; // string
let count = 42; // number
let items = [1, 2, 3]; // number[]
// Inference from return values
function double(n: number) {
return n * 2; // returns number
}
// Contextual typing
const names = ["Alice", "Bob"];
names.forEach(name => {
console.log(name.toUpperCase()); // name: string
});
Opts out of type checking entirely. Avoid in production code — defeats the purpose of TypeScript.
let x: any = "hello";
x = 42; // no error
x.foo(); // no error (crashes at runtime)
Type-safe counterpart of any. Must narrow before use.
let x: unknown = getInput();
// x.toUpperCase(); // Error!
if (typeof x === "string") {
x.toUpperCase(); // OK after narrowing
}
Represents values that never occur. Used for exhaustive checks and functions that never return.
function fail(msg: string): never {
throw new Error(msg);
}
// Exhaustive check
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return /*...*/;
case "square": return /*...*/;
default: const _: never = s;
}
}
interface User {
id: number;
name: string;
email?: string; // optional
readonly createdAt: Date; // immutable
}
// Extending
interface Admin extends User {
permissions: string[];
}
// Declaration merging (interfaces only)
interface User {
avatar?: string; // merges with above
}
type ID = string | number; // union type
type Point = {
x: number;
y: number;
};
// Intersection types
type Timestamped = Point & {
createdAt: Date;
};
// Mapped from existing types
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
TypeScript uses structural typing (duck typing). If the shape matches, it's assignable — regardless of declared type name.
interface Point { x: number; y: number }
interface Coord { x: number; y: number }
let p: Point = { x: 1, y: 2 };
let c: Coord = p; // OK — same shape
type Result =
| { status: "ok"; data: string }
| { status: "error"; message: string };
function handle(r: Result) {
if (r.status === "ok") {
console.log(r.data); // narrowed
} else {
console.log(r.message); // narrowed
}
}
// Typed parameters and return
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Optional & default parameters
function log(msg: string, level = "info"): void {
console.log(`[${level}] ${msg}`);
}
// Rest parameters
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// Function type alias
type Comparator<T> = (a: T, b: T) => number;
// Overload signatures
function parse(input: string): number;
function parse(input: string[]): number[];
// Implementation signature
function parse(input: string | string[]): number | number[] {
if (Array.isArray(input)) {
return input.map(Number);
}
return Number(input);
}
parse("42"); // returns number
parse(["1", "2"]); // returns number[]
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // number | undefined
first(["a", "b"]); // string | undefined
function fetchData<T>(url: string, transform: (raw: unknown) => T): Promise<T> {
return fetch(url).then(res => res.json()).then(transform);
}
// Usage — TS infers return as Promise<User[]>
const users = await fetchData("/api/users", (data) => data as User[]);
// Basic generic
interface Box<T> {
value: T;
}
const strBox: Box<string> = { value: "hello" };
// Constraints with extends
function getLength<T extends { length: number }>(
item: T
): number {
return item.length;
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // Error: no .length
// keyof constraint
function getProperty<T, K extends keyof T>(
obj: T, key: K
): T[K] {
return obj[key];
}
| Utility | Description | Example |
|---|---|---|
Partial<T> | All props optional | Partial<User> |
Required<T> | All props required | Required<Config> |
Pick<T,K> | Select subset of keys | Pick<User, "id" | "name"> |
Omit<T,K> | Remove keys | Omit<User, "password"> |
Record<K,V> | Map keys to values | Record<string, number> |
Readonly<T> | All props readonly | Readonly<State> |
ReturnType<F> | Infer return type | ReturnType<typeof fn> |
Parameters<F> | Infer param types | Parameters<typeof fn> |
// API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
}
async function fetchApi<T>(
url: string
): Promise<ApiResponse<T>> {
const res = await fetch(url);
const data = await res.json();
return { data, status: res.status, timestamp: new Date() };
}
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
let d: Direction = Direction.Up;
console.log(Direction[0]); // "Up"
// Reverse mapping works
enum Status {
Active = "ACTIVE",
Paused = "PAUSED",
Deleted = "DELETED",
}
// No reverse mapping
// More readable in logs/JSON
// Use for API contracts
const enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
}
// Inlined at compile time
// No runtime object generated
let m = HttpMethod.GET;
// Compiles to: let m = "GET";
// String literal union — no runtime cost
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Status = "idle" | "loading" | "success" | "error";
function request(method: HttpMethod, url: string) { /* ... */ }
request("GET", "/api/users"); // OK
// request("PATCH", "/api"); // Error!
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
}
}
class Animal {
// Access modifiers
public name: string;
protected speed: number;
private _id: number;
constructor(name: string, speed: number) {
this.name = name;
this.speed = speed;
this._id = Math.random();
}
// Shorthand — declare in constructor
// constructor(public name: string) {}
move(): string {
return `${this.name} moves at ${this.speed}km/h`;
}
}
class Dog extends Animal {
constructor(name: string) {
super(name, 40);
}
bark(): string {
return `${this.name} says woof!`;
// this.speed OK (protected)
// this._id Error (private)
}
}
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
area() { return Math.PI * this.radius ** 2; }
perimeter() { return 2 * Math.PI * this.radius; }
}
interface Serializable {
serialize(): string;
deserialize(data: string): void;
}
class Config implements Serializable {
constructor(private data: Record<string, unknown>) {}
serialize(): string {
return JSON.stringify(this.data);
}
deserialize(raw: string): void {
this.data = JSON.parse(raw);
}
}
| Modifier | Class | Subclass | Outside |
|---|---|---|---|
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
function process(value: string | number | Date) {
// typeof guard
if (typeof value === "string") {
return value.toUpperCase(); // string
}
// instanceof guard
if (value instanceof Date) {
return value.toISOString(); // Date
}
return value.toFixed(2); // number
}
// "in" operator guard
interface Fish { swim(): void }
interface Bird { fly(): void }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Type predicate function
function isString(val: unknown): val is string {
return typeof val === "string";
}
// Usage — narrows the type
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase()); // string
}
}
// Filter with type predicate
const mixed: (string | number)[] = [1, "a", 2, "b"];
const strings = mixed.filter(
(x): x is string => typeof x === "string"
);
// strings: string[]
function assertDefined<T>(
val: T | null | undefined,
msg?: string
): asserts val is T {
if (val == null) throw new Error(msg ?? "Undefined");
}
const user = getUser(); // User | null
assertDefined(user, "User not found");
user.name; // OK — narrowed to User
// Make all properties optional (how Partial works)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Remap keys with "as"
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// { getName: () => string; getAge: () => number }
type EventName = `${"click" | "focus"}_${"start" | "end"}`;
// "click_start" | "click_end" | "focus_start" | "focus_end"
type CSSValue = `${number}${"px" | "em" | "rem" | "%"}`;
const width: CSSValue = "100px"; // OK
// const bad: CSSValue = "100vw"; // Error
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Extract / Exclude (built-in)
type T1 = Extract<"a" | "b" | "c", "a" | "c">; // "a" | "c"
type T2 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
// Extract return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R
? R
: never;
type R1 = MyReturnType<() => string>; // string
// Unpack Promise
type Awaited<T> = T extends Promise<infer U>
? Awaited<U> // recursive for nested Promises
: T;
type R2 = Awaited<Promise<Promise<number>>>; // number
// Named exports
export interface User { id: number; name: string; }
export function createUser(name: string): User {
return { id: Date.now(), name };
}
// Default export
export default class UserService { /* ... */ }
// Re-export
export { User as AppUser } from "./models";
export * from "./utils";
// Type-only imports (erased at compile time)
import type { User } from "./models";
import { type User, createUser } from "./models";
Type definitions for JavaScript libraries. Contain only type information, no implementation.
// globals.d.ts
declare const API_URL: string;
declare function analytics(event: string): void;
// module.d.ts
declare module "my-lib" {
export function parse(input: string): object;
export const version: string;
}
npm i -D @types/express — community typesnode_modules/@typestypeRoots in tsconfig to customise// Handle non-TS imports (CSS, SVG, JSON)
declare module "*.css" {
const classes: Record<string, string>;
export default classes;
}
declare module "*.svg" {
const src: string;
export default src;
}
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"paths": {
"@/*": ["./src/*"],
"@utils/*": ["./src/utils/*"]
}
},
"include": ["src/**/*"]
}
| Tool | Command | Notes |
|---|---|---|
| ts-node | npx ts-node src/index.ts | JIT compilation, slower startup |
| tsx | npx tsx src/index.ts | esbuild-powered, fast |
| tsc --watch | npx tsc -w | Compile + nodemon for restart |
| Node 22+ | node --experimental-strip-types index.ts | Native TS stripping (experimental) |
// package.json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
// tsconfig-paths registers @ aliases at runtime
// tsx handles them automatically
npm i -D typescript @types/node tsx
npm i -D @tsconfig/node22 # shared base config
import express, {
Request, Response, NextFunction
} from "express";
interface CreateUserBody {
name: string;
email: string;
}
interface UserParams {
id: string;
}
// Typed route handler
app.post(
"/api/users",
(req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email } = req.body; // typed!
// ...
res.status(201).json({ id: 1, name, email });
}
);
app.get(
"/api/users/:id",
(req: Request<UserParams>, res: Response) => {
const userId = req.params.id; // string
// ...
}
);
// Extend Express Request
declare global {
namespace Express {
interface Request {
user?: { id: number; role: string };
}
}
}
// Auth middleware
function auth(
req: Request, res: Response, next: NextFunction
): void {
const token = req.headers.authorization;
if (!token) {
res.status(401).json({ error: "Unauthorized" });
return;
}
req.user = verifyToken(token); // typed!
next();
}
// Error middleware (4 params)
function errorHandler(
err: Error, req: Request,
res: Response, _next: NextFunction
): void {
res.status(500).json({ error: err.message });
}
npm i express
npm i -D @types/express typescript tsx
// Props interface
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: (e: React.MouseEvent) => void;
children?: React.ReactNode;
}
// Function component (preferred over React.FC)
function Button({ label, variant = "primary",
disabled, onClick, children }: ButtonProps) {
return (
<button
className={variant}
disabled={disabled}
onClick={onClick}
>
{children ?? label}
</button>
);
}
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
// Usage — T inferred as User
<List items={users} renderItem={u => <li>{u.name}</li>} />
// useState with type parameter
const [user, setUser] = useState<User | null>(null);
// useRef
const inputRef = useRef<HTMLInputElement>(null);
// useReducer
type Action =
| { type: "increment" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "set": return action.payload;
}
}
const [count, dispatch] = useReducer(reducer, 0);
| Event | Type |
|---|---|
| Click | React.MouseEvent<HTMLButtonElement> |
| Change | React.ChangeEvent<HTMLInputElement> |
| Submit | React.FormEvent<HTMLFormElement> |
| Keyboard | React.KeyboardEvent<HTMLInputElement> |
"strict": true Enables| Flag | Effect |
|---|---|
strictNullChecks | null/undefined not assignable to other types |
noImplicitAny | Error on inferred any |
strictFunctionTypes | Contravariant parameter checking |
strictPropertyInitialization | Class props must be initialised |
noImplicitThis | Error on this with implicit any |
alwaysStrict | Emit "use strict" |
useUnknownInCatchVariables | catch(e) is unknown |
{
"compilerOptions": {
// Type checking
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
// Output
"target": "ES2022",
"module": "Node16",
"outDir": "./dist",
"declaration": true,
"sourceMap": true,
// Module resolution
"moduleResolution": "Node16",
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
// Emit
"removeComments": true,
"skipLibCheck": true
}
}
Start every new project with "strict": true. Add "noUncheckedIndexedAccess": true for array/object safety. Use "exactOptionalPropertyTypes": true to distinguish undefined from missing. These flags catch entire categories of bugs at zero runtime cost.
// Discriminated union for success / failure
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJSON(input: string): Result<unknown> {
try {
return { ok: true, value: JSON.parse(input) };
} catch (e) {
return { ok: false, error: e as Error };
}
}
const result = parseJSON('{"name":"Alice"}');
if (result.ok) {
console.log(result.value); // narrowed to unknown
} else {
console.error(result.error.message);
}
// Prevent mixing IDs from different domains
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
function createUserId(id: string): UserId {
// validate...
return id as UserId;
}
function getUser(id: UserId) { /* ... */ }
// getUser(orderId); // Error — type mismatch!
type Status = "active" | "paused" | "deleted";
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function handleStatus(status: Status): string {
switch (status) {
case "active": return "Running";
case "paused": return "On hold";
case "deleted": return "Removed";
default: return assertNever(status);
// If a new status is added, TS errors here
}
}
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number
) {
super(message);
this.name = "AppError";
}
}
class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, "NOT_FOUND", 404);
}
}
class ValidationError extends AppError {
constructor(public readonly fields: string[]) {
super("Validation failed", "VALIDATION", 400);
}
}
Migrating a JavaScript project to TypeScript doesn't have to be all-or-nothing. Use a gradual, incremental approach.
tsconfig.json with "allowJs": true.js → .ts one at a time"strict": false, enable flags incrementallyany with unknown, then add proper types"strict": true once most files are typed// @ts-check — enables TS checking in JS files
/** @type {string} */
let name = "Alice";
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"strict": false,
"noImplicitAny": false,
"target": "ES2022",
"module": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
| Phase | Action | Risk |
|---|---|---|
| 1. Setup | allowJs + checkJs | Zero |
| 2. Annotate | JSDoc types in .js files | Zero |
| 3. Rename | .js → .ts, fix errors | Low |
| 4. Strict flags | Enable one flag at a time | Medium |
| 5. Full strict | "strict": true | Low (if incremental) |
ts-migrate (Airbnb) — auto-converts JS to TS@ts-expect-error — suppress known issues temporarily// @ts-ignore — last resort, avoid in productionany/unknown/neverinfertsx, path aliases, tsconfigallowJs, incremental adoption"strict": true on every new projectunknown over any — always