Basics
Basic Types
TypeScript provides primitive types: string, number, boolean, null, undefined, symbol, bigint. Also arrays, tuples, and any.
// Primitives
let name: string = "Alice";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// Arrays
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];
// Tuples - fixed length and types
let tuple: [string, number] = ["Alice", 30];
// any - turns off type checking (avoid!)
let anything: any = "string";
anything = 42; // No error
💡 Key Point: Avoid 'any' when possible. Use 'unknown' for truly dynamic values, or narrow with type guards. Tuples are useful for fixed-structure data like coordinates [x, y].
Basics
Interfaces
Interfaces define the shape of objects. They support optional properties, readonly properties, and can be extended. Preferred for object shapes and classes.
interface User {
id: number;
name: string;
email?: string; // optional
readonly created: Date; // immutable
}
const user: User = {
id: 1,
name: "Alice",
created: new Date()
};
// user.created = new Date(); // Error: readonly
// Extending interfaces
interface Admin extends User {
role: string;
permissions: string[];
}
// Index signatures for dynamic keys
interface StringMap {
[key: string]: string;
}
💡 Key Point: Interfaces can be declared multiple times and TypeScript will merge them. Use interfaces for public API contracts and object shapes.
Basics
Type Aliases
Type aliases create custom type names. Unlike interfaces, they work for unions, intersections, primitives, and tuples. Can't be extended or merged.
// Union types
type Status = "pending" | "success" | "error";
// Intersection types
type Person = { name: string };
type Employee = { employeeId: number };
type Worker = Person & Employee;
const worker: Worker = {
name: "Bob",
employeeId: 123
};
// Function types
type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;
// Complex types
type ApiResponse<T> = {
data: T;
error?: string;
status: number;
};
💡 Key Point: Use type aliases for unions, intersections, and primitives. Use interfaces for object shapes and when you need declaration merging.
Advanced
Generics
Generics allow you to write reusable, type-safe code that works with multiple types. Use <T> syntax to create type parameters.
// Generic function
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // T = string
identity(42); // T inferred as number
// Generic interface
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
// Constraints - T must have 'length'
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("hello"); // OK
logLength([1, 2, 3]); // OK
// logLength(123); // Error: no length property
// Multiple type parameters
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
💡 Key Point: Generics are essential for creating reusable components and utilities. Use constraints to enforce specific capabilities on generic types.
Advanced
Union & Intersection Types
Unions (|) represent values that can be one of several types. Intersections (&) combine multiple types into one.
// Union - can be string OR number
type ID = string | number;
function printId(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
// Discriminated unions (tagged unions)
type Success = { status: "success"; data: string };
type Error = { status: "error"; error: string };
type Result = Success | Error;
function handleResult(result: Result) {
if (result.status === "success") {
console.log(result.data); // TypeScript knows it's Success
} else {
console.log(result.error); // TypeScript knows it's Error
}
}
// Intersection - must satisfy ALL types
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const person: Person = { name: "Alice", age: 30 };
💡 Key Point: Discriminated unions are powerful for modeling state machines and API responses. Use a common literal property (like 'status') to discriminate between types.
Advanced
Type Guards
Type guards narrow down types in conditional blocks. Built-in: typeof, instanceof, in. Custom guards use 'is' predicates.
// typeof guard
function isString(value: unknown): value is string {
return typeof value === "string";
}
// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// in operator guard
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
// Custom type guard with 'is'
function isNumber(value: any): value is number {
return typeof value === "number" && !isNaN(value);
}
💡 Key Point: Type guards are essential for working with union types safely. Custom type guards with 'is' predicates give you full control over narrowing.
Advanced
Utility Types
TypeScript provides built-in utility types to transform existing types: Partial, Required, Readonly, Pick, Omit, Record, and more.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial - all properties optional
type PartialUser = Partial<User>;
const update: PartialUser = { name: "Alice" };
// Required - all properties required
type RequiredUser = Required<Partial<User>>;
// Readonly - all properties immutable
type ReadonlyUser = Readonly<User>;
// Pick - select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// Record - create object type with specific keys
type Roles = "admin" | "user" | "guest";
type Permissions = Record<Roles, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
// ReturnType - extract return type of function
function getUser() { return { id: 1, name: "Alice" }; }
type User2 = ReturnType<typeof getUser>;
💡 Key Point: Utility types save you from manually redefining types. They're especially useful for forms, API updates, and derived types.
Basics
Enums
Enums define a set of named constants. Numeric enums auto-increment, string enums require explicit values. Prefer const enums for tree-shaking.
// Numeric enum (auto-increment from 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
let dir: Direction = Direction.Up;
// String enum (must specify all values)
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Error = "ERROR"
}
// Const enum (no runtime code, inlined at compile time)
const enum Color {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF"
}
let red: Color = Color.Red; // Inlined as "#FF0000"
// Heterogeneous enum (mixed - avoid!)
enum Mixed {
No = 0,
Yes = "YES"
}
💡 Key Point: Prefer string enums for clarity and debuggability. Use const enums for better tree-shaking. Avoid heterogeneous enums.
Basics
Literal Types
Literal types represent exact values instead of general types. Combine with unions to create precise type definitions.
// String literals
type Alignment = "left" | "center" | "right";
let align: Alignment = "center"; // OK
// align = "top"; // Error: not in union
// Numeric literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 3;
// Boolean literals (less common)
type AlwaysTrue = true;
let mustBeTrue: AlwaysTrue = true;
// mustBeTrue = false; // Error
// Template literal types (TS 4.1+)
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Combining literals with interfaces
interface Config {
mode: "development" | "production";
port: 3000 | 8080;
}
💡 Key Point: Literal types are perfect for configuration objects, API responses, and state machines. Template literal types unlock powerful string manipulation at the type level.
Advanced
never & unknown
'never' represents values that never occur (unreachable code). 'unknown' is a type-safe alternative to 'any' - must narrow before use.
// never - for exhaustive checks
type Shape = { kind: "circle" } | { kind: "square" };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return 0;
case "square": return 0;
default:
// If you add a new shape and forget to handle it,
// this will be a compile error
const _exhaustive: never = shape;
throw new Error("Unhandled shape");
}
}
// unknown - type-safe any
function processValue(value: unknown) {
// Can't use value directly
// console.log(value.toString()); // Error
// Must narrow first
if (typeof value === "string") {
console.log(value.toUpperCase()); // OK
} else if (typeof value === "number") {
console.log(value.toFixed(2)); // OK
}
}
// never for functions that never return
function throwError(msg: string): never {
throw new Error(msg);
}
💡 Key Point: Use 'never' for exhaustive checks in switch statements. Use 'unknown' instead of 'any' when you need to handle arbitrary values safely.
Advanced
Mapped Types
Mapped types transform properties of existing types. Loop over keys and apply transformations. Foundation of utility types.
// Make all properties optional
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type Readonly<T> = {
[K in keyof T]: readonly T[K];
};
// Custom mapped type - add 'get' prefix
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
// Conditional mapped types
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Filter properties by type
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
💡 Key Point: Mapped types are incredibly powerful for code generation and type transformations. Master them to build advanced type utilities.
Advanced
Conditional Types
Conditional types select types based on conditions. Use 'extends' keyword with ternary syntax: T extends U ? X : Y.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Extract non-nullable types
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null>; // string
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { id: 1, name: "Alice" }; }
type User = ReturnType<typeof getUser>; // { id: number; name: string }
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// string[] | number[] (not (string | number)[])
// infer keyword - extract types
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnpackPromise<Promise<string>>; // string
💡 Key Point: Conditional types unlock type-level programming. The 'infer' keyword lets you extract and capture types. Essential for advanced type utilities.
Advanced
Decorators
Decorators are annotations that modify classes, methods, properties, or parameters. Enable in tsconfig with 'experimentalDecorators'.
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class User {
name: string = "Alice";
}
// Method decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number) {
return a + b;
}
}
// Property decorator
function readonly(target: any, key: string) {
Object.defineProperty(target, key, {
writable: false
});
}
class Product {
@readonly
price: number = 100;
}
💡 Key Point: Decorators are experimental but widely used in frameworks like Angular and NestJS. Stage 3 decorators are coming to standard JavaScript.
Basics
Modules & Namespaces
TypeScript supports ES6 modules (import/export). Namespaces are older TS-specific feature for organizing code. Prefer modules.
// ES6 Modules (preferred)
// user.ts
export interface User {
id: number;
name: string;
}
export function getUser(id: number): User {
return { id, name: "Alice" };
}
// main.ts
import { User, getUser } from "./user";
// Default exports
// utils.ts
export default function log(msg: string) {
console.log(msg);
}
// main.ts
import log from "./utils";
// Namespaces (older, avoid in new code)
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export class EmailValidator implements StringValidator {
isValid(s: string) {
return s.includes("@");
}
}
}
💡 Key Point: Always prefer ES6 modules over namespaces. Namespaces were created before ES6 modules existed. Use them only for declaration merging or legacy code.
Basics
Type Assertions
Type assertions tell TypeScript to treat a value as a specific type. Two syntaxes: 'as' and angle brackets. Use sparingly - doesn't change runtime.
// as syntax (preferred in JSX)
let value: any = "hello";
let length: number = (value as string).length;
// Angle bracket syntax (doesn't work in JSX)
let length2: number = (<string>value).length;
// Common use case - DOM elements
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test@example.com";
// Non-null assertion (!)
function getValue(key: string): string | undefined {
return key;
}
let name = getValue("name")!; // Assert it's not undefined
// Dangerous - use only when you're 100% sure
// Const assertions
let obj = {
name: "Alice",
age: 30
} as const;
// obj.name = "Bob"; // Error: readonly
let arr = [1, 2, 3] as const; // readonly [1, 2, 3]
💡 Key Point: Type assertions are escape hatches. They don't change runtime behavior. Use const assertions for immutable literals and known values.
Basics
Classes & Access Modifiers
TypeScript classes support access modifiers (public, private, protected), abstract classes, and implements for interfaces.
class User {
public id: number; // accessible anywhere (default)
private password: string; // only within this class
protected email: string; // this class + subclasses
readonly created: Date; // immutable after initialization
constructor(id: number, password: string, email: string) {
this.id = id;
this.password = password;
this.email = email;
this.created = new Date();
}
private hashPassword(): string {
return "hashed_" + this.password;
}
login(pwd: string): boolean {
return this.hashPassword() === "hashed_" + pwd;
}
}
// Shorthand - parameter properties
class Product {
constructor(
public id: number,
private price: number,
readonly name: string
) {}
}
// Abstract classes
abstract class Animal {
abstract makeSound(): void; // must be implemented
move(): void {
console.log("Moving...");
}
}
class Dog extends Animal {
makeSound() { console.log("Woof!"); }
}
💡 Key Point: TypeScript's private/protected are compile-time only. For true privacy, use JavaScript's # private fields. Parameter properties are great for reducing boilerplate.
Advanced
Declaration Files (.d.ts)
Declaration files provide type definitions for JavaScript libraries. Use declare keyword for ambient declarations.
// types.d.ts - ambient type declarations
declare module "my-library" {
export function greet(name: string): string;
export const version: string;
}
// Global variable declarations
declare const API_URL: string;
declare const VERSION: number;
// Augmenting existing types
// express-custom.d.ts
import "express";
declare module "express" {
interface Request {
user?: {
id: number;
name: string;
};
}
}
// Now req.user is typed
app.get("/", (req, res) => {
console.log(req.user?.name); // TypeScript knows about user
});
// Triple-slash directives
/// <reference path="./types.d.ts" />
/// <reference types="node" />
💡 Key Point: Use @types packages from DefinitelyTyped for popular libraries. Create .d.ts files for custom JavaScript libraries or to augment existing types.
Configuration
tsconfig.json
TypeScript configuration file. Controls compiler options, file inclusion/exclusion, and project structure.
{
"compilerOptions": {
// Language & Environment
"target": "ES2022", // JS version to emit
"lib": ["ES2022", "DOM"], // Built-in type definitions
"jsx": "react-jsx", // JSX compilation
// Modules
"module": "ESNext", // Module system
"moduleResolution": "bundler", // How TS resolves imports
"baseUrl": ".", // Base for relative imports
"paths": { // Path aliases
"@/*": ["./src/*"]
},
// Type Checking
"strict": true, // Enable all strict checks
"noImplicitAny": true, // Error on implicit any
"strictNullChecks": true, // null/undefined checks
"strictFunctionTypes": true, // Function param checks
"noUnusedLocals": true, // Warn on unused variables
"noUnusedParameters": true, // Warn on unused params
// Emit
"outDir": "./dist", // Output directory
"sourceMap": true, // Generate .map files
"declaration": true, // Generate .d.ts files
"removeComments": true, // Strip comments
// Interop
"esModuleInterop": true, // Better CJS/ESM interop
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
// Skip Checking
"skipLibCheck": true // Skip .d.ts type checking
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
💡 Key Point: Start with 'strict: true' for new projects. Use 'paths' for cleaner imports. Enable 'skipLibCheck' to speed up compilation.
Integration
React with TypeScript
Type React components with props, state, events, and hooks. Use FC type or function declarations.
import { useState, useEffect } from "react";
// Props interface
interface UserCardProps {
name: string;
age: number;
email?: string;
onUpdate?: (name: string) => void;
}
// Function component with props
function UserCard({ name, age, email, onUpdate }: UserCardProps) {
const [count, setCount] = useState<number>(0);
useEffect(() => {
console.log("Mounted");
}, []);
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget);
onUpdate?.(name);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<div>
<h1>{name}</h1>
<input onChange={handleChange} />
<button onClick={handleClick}>Update</button>
</div>
);
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
export default UserCard;
💡 Key Point: Use React.FC sparingly - it's often better to type props directly. TypeScript infers event types from the element (e.g., onChange on input).
Patterns
TypeScript Best Practices
Write maintainable TypeScript: prefer inference, avoid any, use const assertions, and leverage utility types.
// ✅ Prefer type inference
const user = { id: 1, name: "Alice" }; // Type inferred
// ❌ Don't over-annotate
const user2: { id: number; name: string } = { id: 1, name: "Alice" };
// ✅ Use unknown instead of any
function parse(json: string): unknown {
return JSON.parse(json);
}
// ✅ Const assertions for literal types
const routes = {
home: "/",
about: "/about"
} as const;
type Routes = typeof routes; // { readonly home: "/"; readonly about: "/about" }
// ✅ Use discriminated unions for state
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
// ✅ Prefer interfaces for objects, types for unions
interface User { name: string; }
type Status = "active" | "inactive";
// ✅ Use branded types for type safety
type UserId = number & { readonly brand: unique symbol };
type ProductId = number & { readonly brand: unique symbol };
function getUser(id: UserId) {}
// getUser(123); // Error - number is not UserId
💡 Key Point: Enable 'strict' mode. Avoid 'any' like the plague. Use const assertions and branded types for extra safety. TypeScript is a tool - use it wisely.