TypeScript is a superset of JavaScript that adds static typing. It catches errors at compile time rather than runtime, provides world-class IDE autocomplete, and serves as self-documenting code. This guide takes you from the basics to the most advanced generic and conditional types used by library authors.
Step 1 — Basic Types & Annotations
TypeScript infers types in most cases, but you can explicitly annotate them when needed.
// Basic primitives
let isDone: boolean = false;
let age: number = 25;
let userName: string = 'Alice';
// Arrays
let list: number[] = [1, 2, 3];
let genericList: Array<number> = [1, 2, 3];
// Tuples (fixed-length arrays with known types)
let user: [number, string] = [1, 'Alice'];
// user[0] is number, user[1] is string
// any vs unknown
let loose: any = 4;
loose.ifItExists(); // No compiler error, but likely runtime error
let safer: unknown = 4;
// safer.ifItExists(); // Error: Object is of type 'unknown'
if (typeof safer === 'string') {
console.log(safer.toUpperCase()); // Narrowed to string
}
// never: functions that never return (throw or infinite loop)
function throwError(msg: string): never {
throw new Error(msg);
}
// void: functions that return nothing
function logInfo(msg: string): void {
console.log(msg);
}Step 2 — Interfaces vs Types
Interfaces and Type Aliases both define object shapes, but have slight differences in capabilities.
// Type Alias: Good for unions, primitives, and mapped types
type ID = string | number;
type Status = 'pending' | 'success' | 'error'; // Literal union
type User = {
id: ID;
name: string;
status: Status;
};
// Interface: Good for object shapes, class implementation, and declaration merging
interface Animal {
name: string;
speak(): void;
}
// Extending
interface Dog extends Animal {
breed: string;
}
type Cat = Animal & { livesLeft: number }; // Intersection
// Interface merging (unique to interfaces)
interface Window {
title: string;
}
interface Window {
ts: boolean;
}
// Window now has both title and ts properties.Step 3 — Functions & Overloads
// Param and return types
function add(a: number, b: number): number {
return a + b;
}
// Optional (?), Default (=), and Rest (...)
function greet(name: string, title?: string, greeting = 'Hello') {
return `${greeting} ${title ? title + ' ' : ''}${name}`;
}
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Function Overloads: Multiple signatures for one implementation
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp - 1, d);
}
return new Date(mOrTimestamp);
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);Step 4 — Generics Deep Dive
Generics are variables for types. They allow you to write reusable, type-safe code that works across multiple data types.
// Generic Function
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(5);
const str = identity('Hello'); // T inferred as string
// Generic Interface
interface ApiResponse<T> {
status: number;
data: T;
error?: string;
}
interface User { id: number; name: string; }
const response: ApiResponse<User> = {
status: 200,
data: { id: 1, name: 'Alice' }
};
// Generic Constraints (extends)
// T must have a .length property
function logLength<T extends { length: number }>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength([1, 2, 3]); // Arrays have length
logLength('Hello'); // Strings have length
// logLength(5); // Error: number doesn't have length
// Using multiple generics
function merge<U, V>(obj1: U, obj2: V): U & V {
return { ...obj1, ...obj2 };
}Step 5 — Built-in Utility Types
TypeScript provides several utility types to facilitate common type transformations.
interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
}
// Partial: makes all properties optional
type PartialTodo = Partial<Todo>;
// Required: makes all properties required
type RequiredTodo = Required<Todo>;
// Pick: select specific properties
type TodoPreview = Pick<Todo, 'id' | 'title'>;
// Omit: remove specific properties
type TodoInfo = Omit<Todo, 'id'>;
// Record: create a dictionary type
type RoleMap = Record<string, TodoPreview>;
const roles: RoleMap = {
task1: { id: 1, title: 'Learn TS' }
};
// Extracting function types
function getUser() { return { id: 1, name: 'A' }; }
type ReturnTypeObj = ReturnType<typeof getUser>; // { id: number, name: string }
// Awaited: unwrap a Promise type
type AsyncResult = Awaited<Promise<string>>; // stringStep 6 — Conditional Types & 'infer'
Conditional types act like 'if/else' statements for types.
// Syntax: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
// The 'infer' keyword extracts a type from another type
type UnpackArray<T> = T extends Array<infer U> ? U : T;
type StringArr = UnpackArray<string[]>; // string
type Num = UnpackArray<number>; // number
// Distributive Conditional Types
// If T is a union, the condition is applied to each member individually
type NonNullable<T> = T extends null | undefined ? never : T;
type SafeUnion = NonNullable<string | number | null | undefined>;
// Result: string | numberStep 7 — Mapped Types & Template Literals
type Features = {
darkMode: boolean;
newUserProfile: boolean;
};
// Mapped Type: iterates over keys
type OptionsFlags<T> = {
[Property in keyof T]: boolean;
};
type FeatureFlags = OptionsFlags<Features>;
// Template Literal Types: manipulate strings at the type level
type Color = 'red' | 'blue';
type Shade = 'light' | 'dark';
type ColorPalette = `${Shade}-${Color}`;
// 'light-red' | 'light-blue' | 'dark-red' | 'dark-blue'
// Key Remapping via 'as'
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }Step 8 — tsconfig.json Best Practices
A strong tsconfig.json is critical for catching bugs.
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type */
"strictNullChecks": true, /* Enable strict null checks */
"strictFunctionTypes": true,
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals */
"noUnusedParameters": true, /* Report errors on unused parameters */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value */
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true, /* Skip type checking of declaration files (.d.ts) for speed */
"forceConsistentCasingInFileNames": true
}
}