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.

basics.tstypescript
// 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.

types.tstypescript
// 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

functions.tstypescript
// 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.

generics.tstypescript
// 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.

utilities.tstypescript
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>>; // string

Step 6 — Conditional Types & 'infer'

Conditional types act like 'if/else' statements for types.

conditional.tstypescript
// 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 | number

Step 7 — Mapped Types & Template Literals

mapped.tstypescript
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.

tsconfig.jsonjson
{
  "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
  }
}