Back to Articles
14 min read

Mastering TypeScript Fundamentals: The Comprehensive Guide

Transitioning to strong typing? This deep dive covers the essential TypeScript landscape: environment setup, basic and complex types, function signatures, and the mechanics of type inference. Build a solid foundation for scalable software engineering.

TypeScript Installation and Setup

TypeScript is installed via npm (Node Package Manager) and compiles to JavaScript using the tsc compiler. Install globally with npm install -g typescript, then verify with tsc --version. For project-specific installation, use npm install --save-dev typescript which is the recommended approach for team projects.

# Global installation npm install -g typescript # Project installation mkdir my-project && cd my-project npm init -y npm install --save-dev typescript npx tsc --init # Creates tsconfig.json npx tsc app.ts # Compiles app.ts → app.js

tsconfig.json Configuration

The tsconfig.json file is the central configuration for your TypeScript project, controlling compiler options, file inclusion/exclusion, and strict type-checking rules. Key options include target (JS version output), module (module system), strict (enables all strict checks), and outDir (output directory).

{ "compilerOptions": { "target": "ES2020", // Output JS version "module": "commonjs", // Module system "strict": true, // Enable all strict checks "outDir": "./dist", // Output directory "rootDir": "./src", // Source directory "esModuleInterop": true, // CommonJS/ES module compat "skipLibCheck": true, // Skip .d.ts checking "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }
┌─────────────────────────────────────────┐
│           tsconfig.json                 │
├─────────────────────────────────────────┤
│  compilerOptions ──► How to compile     │
│  include         ──► What to compile    │
│  exclude         ──► What to ignore     │
└─────────────────────────────────────────┘

Basic Types (string, number, boolean)

TypeScript's three primitive types mirror JavaScript: string for text, number for all numeric values (integers and floats), and boolean for true/false values. Unlike some languages, TypeScript has only one number type—no separate int, float, or double.

// Explicit type annotations let username: string = "alice"; let age: number = 30; let isActive: boolean = true; // Numbers include integers, floats, hex, binary, octal let decimal: number = 42; let float: number = 3.14; let hex: number = 0xff; let binary: number = 0b1010; // Template literals work with strings let greeting: string = `Hello, ${username}! You are ${age} years old.`;
┌─────────────────────────────────────────┐
│           Primitive Types               │
├─────────────┬─────────────┬─────────────┤
│   string    │   number    │   boolean   │
├─────────────┼─────────────┼─────────────┤
│   "hello"   │     42      │    true     │
│   'world'   │    3.14     │    false    │
│   `text`    │    0xff     │             │
└─────────────┴─────────────┴─────────────┘

Arrays and Tuples

Arrays hold multiple values of the same type using either type[] or Array<type> syntax, while tuples are fixed-length arrays with specific types at each position. Tuples are useful for returning multiple values from functions or representing structured data like coordinates.

// Arrays - two syntax options (equivalent) let numbers: number[] = [1, 2, 3, 4, 5]; let names: Array<string> = ["Alice", "Bob", "Charlie"]; // Array methods are fully typed numbers.push(6); // OK // numbers.push("seven"); // Error: string not assignable to number // Tuples - fixed length and types let coordinate: [number, number] = [10, 20]; let userInfo: [string, number, boolean] = ["Alice", 30, true]; // Accessing tuple elements let x = coordinate[0]; // number let name = userInfo[0]; // string // Named tuples (TS 4.0+) - for documentation let point: [x: number, y: number] = [10, 20]; // Tuple with optional element let flexible: [string, number?] = ["hello"]; // Tuple with rest elements let mixed: [string, ...number[]] = ["scores", 90, 85, 95];
Array:  [ same, same, same, same, ... ]  ← Variable length
         type  type  type  type

Tuple:  [ type1, type2, type3 ]          ← Fixed length
           ↓      ↓      ↓
        [string, number, boolean]
           ↓      ↓      ↓
        ["Alice", 30,   true]

Enums

Enums define a set of named constants, making code more readable and maintainable than using magic numbers or strings. TypeScript offers numeric enums (auto-incrementing from 0), string enums, and heterogeneous enums; numeric enums also support reverse mapping from value to name.

// Numeric enum (auto-increments from 0) enum Direction { Up, // 0 Down, // 1 Left, // 2 Right // 3 } // Numeric enum with custom values enum HttpStatus { OK = 200, BadRequest = 400, NotFound = 404, ServerError = 500 } // String enum (no auto-increment) enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" } // Usage let dir: Direction = Direction.Up; let status: HttpStatus = HttpStatus.OK; // Reverse mapping (numeric enums only) console.log(Direction[0]); // "Up" console.log(Direction.Up); // 0 // const enum - inlined at compile time (more efficient) const enum Size { Small = 1, Medium = 2, Large = 3 } let mySize = Size.Medium; // Compiles to: let mySize = 2;
Numeric Enum:              String Enum:
┌─────────┬───────┐       ┌─────────┬─────────┐
│  Name   │ Value │       │  Name   │  Value  │
├─────────┼───────┤       ├─────────┼─────────┤
│  Up     │   0   │       │  Red    │ "RED"   │
│  Down   │   1   │       │  Green  │ "GREEN" │
│  Left   │   2   │       │  Blue   │ "BLUE"  │
│  Right  │   3   │       └─────────┴─────────┘
└─────────┴───────┘

Any, Unknown, Void, Never, Null, Undefined

These special types handle edge cases: any disables type checking (avoid when possible), unknown is a type-safe alternative requiring type checks before use, void represents no return value, never represents values that never occur (infinite loops, always-throw functions), while null and undefined represent absence of value.

// any - escape hatch, disables type checking (avoid!) let anything: any = 42; anything = "string"; // No error anything.foo.bar; // No error, but runtime crash! // unknown - type-safe any, must narrow before use let uncertain: unknown = 42; // uncertain.toFixed(); // Error! Must check type first if (typeof uncertain === "number") { uncertain.toFixed(); // OK after narrowing } // void - function returns nothing function logMessage(msg: string): void { console.log(msg); // No return statement } // never - function never returns function throwError(msg: string): never { throw new Error(msg); // Always throws } function infiniteLoop(): never { while (true) {} // Never terminates } // null and undefined let nullable: string | null = null; let optional: string | undefined = undefined; // strictNullChecks (recommended) // With this enabled, null/undefined must be explicit
Type Safety Spectrum:
┌─────────────────────────────────────────────────────────┐
│   any        unknown        concrete types              │
│   ◄──────────────────────────────────────────────────►  │
│   Least                                    Most         │
│   Safe                                     Safe         │
└─────────────────────────────────────────────────────────┘

Return Types:
┌─────────────┬───────────────────────────────────────────┐
│    void     │  Function completes, returns nothing      │
│    never    │  Function never completes (throws/loops)  │
└─────────────┴───────────────────────────────────────────┘

Type Inference

TypeScript automatically deduces types from context when you don't explicitly annotate them, making code cleaner while maintaining type safety. The compiler infers types from initialization values, return statements, and contextual typing (like callback parameters); leverage this feature but add explicit types for function signatures and complex objects.

// Variable inference - from initialization let name = "Alice"; // inferred as string let count = 42; // inferred as number let active = true; // inferred as boolean let items = [1, 2, 3]; // inferred as number[] // Function return inference function add(a: number, b: number) { return a + b; // Return type inferred as number } // Contextual typing - from usage context const names = ["Alice", "Bob", "Charlie"]; names.forEach((name) => { // 'name' inferred as string from array type console.log(name.toUpperCase()); }); // Object inference let user = { name: "Alice", // string age: 30, // number active: true // boolean }; // inferred as { name: string; age: number; active: boolean } // Best common type - union when mixed let mixed = [1, "two", 3]; // (string | number)[] // When inference fails, annotate explicitly let data; // any (no initializer) let data2: string; // explicit annotation preferred
Inference Flow:
┌─────────────────────────────────────────┐
│  let x = "hello"                        │
│          ↓                              │
│  TypeScript sees string literal         │
│          ↓                              │
│  x is inferred as: string               │
└─────────────────────────────────────────┘

Type Annotations

Type annotations are explicit type declarations you add to variables, parameters, and return values using a colon (:) followed by the type. While inference handles many cases, explicit annotations improve code documentation, catch errors earlier, and are essential for function parameters and uninitialized variables.

// Variable annotations let username: string = "Alice"; let age: number = 30; let isAdmin: boolean = false; // Annotation without initialization let email: string; // Must annotate when no initial value email = "alice@example.com"; // Function parameter and return annotations function greet(name: string, age: number): string { return `Hello, ${name}! You are ${age} years old.`; } // Object type annotation let user: { name: string; age: number; email?: string } = { name: "Alice", age: 30 }; // Array annotations let scores: number[] = [90, 85, 95]; let names: Array<string> = ["Alice", "Bob"]; // Function type annotation let calculator: (a: number, b: number) => number; calculator = (x, y) => x + y; // When to annotate vs rely on inference let inferred = "hello"; // inference is fine function process(data: unknown): void { // annotate params/returns // ... }
Annotation Syntax:
┌────────────────────────────────────────────────────┐
│  let variableName: Type = value;                   │
│                    ↑                               │
│              Annotation                            │
│                                                    │
│  function name(param: Type): ReturnType { }       │
│                      ↑            ↑                │
│              Parameter      Return Type            │
└────────────────────────────────────────────────────┘

Functions and Function Types

TypeScript functions include typed parameters, return types, and can be stored in variables with function type expressions. The syntax (params) => returnType defines a function type; use void for no return value and always type parameters since they cannot be inferred.

// Basic function with types function add(a: number, b: number): number { return a + b; } // Arrow function const multiply = (a: number, b: number): number => a * b; // Function type expression let mathOp: (x: number, y: number) => number; mathOp = add; mathOp = multiply; // Function type alias type BinaryOperation = (a: number, b: number) => number; const divide: BinaryOperation = (a, b) => a / b; // Function returning void function logMessage(msg: string): void { console.log(msg); } // Function with callback function processArray( items: number[], callback: (item: number) => void ): void { items.forEach(callback); } // Higher-order function function createMultiplier(factor: number): (n: number) => number { return (n) => n * factor; } const double = createMultiplier(2); console.log(double(5)); // 10
Function Type Syntax:
┌───────────────────────────────────────────────────┐
│  (param1: Type1, param2: Type2) => ReturnType     │
│   ↑                              ↑                │
│   Parameters                     Return           │
│                                                   │
│  Example:                                         │
│  (a: number, b: number) => number                 │
└───────────────────────────────────────────────────┘

Optional and Default Parameters

Optional parameters (marked with ?) may be omitted when calling a function and receive undefined if not provided, while default parameters have a fallback value and are implicitly optional. Optional parameters must come after required ones, but default parameters can appear anywhere.

// Optional parameter (?) function greet(name: string, greeting?: string): string { return `${greeting || "Hello"}, ${name}!`; } greet("Alice"); // "Hello, Alice!" greet("Alice", "Hi"); // "Hi, Alice!" // Default parameter (= value) function greetDefault(name: string, greeting: string = "Hello"): string { return `${greeting}, ${name}!`; } greetDefault("Alice"); // "Hello, Alice!" greetDefault("Alice", "Hi"); // "Hi, Alice!" // Default can be any expression function createUser( name: string, role: string = "user", createdAt: Date = new Date() ) { return { name, role, createdAt }; } // Optional vs Default function demo( required: string, optional?: number, // undefined if omitted defaulted: boolean = true // true if omitted ) { console.log({ required, optional, defaulted }); } // Rest parameters (must be last) function sum(...numbers: number[]): number { return numbers.reduce((acc, n) => acc + n, 0); } sum(1, 2, 3, 4); // 10
Parameter Order:
┌──────────────────────────────────────────────────┐
│  function fn(required, optional?, ...rest) {}    │
│              ↑         ↑          ↑              │
│           Must have  Can omit   Collects rest    │
│                                                  │
│  function fn(required, defaulted = value) {}     │
│                        ↑                         │
│                 Uses default if omitted          │
└──────────────────────────────────────────────────┘

Object Types

Object types define the shape of an object by specifying property names and their types, using either inline syntax with curly braces or type aliases/interfaces. Properties can be marked optional with ? or readonly with readonly; TypeScript uses structural typing, meaning objects are compatible if they have matching shapes.

// Inline object type function printUser(user: { name: string; age: number }): void { console.log(`${user.name} is ${user.age} years old`); } // Optional properties let config: { host: string; port?: number } = { host: "localhost" // port is optional }; // Readonly properties let point: { readonly x: number; readonly y: number } = { x: 10, y: 20 }; // point.x = 30; // Error: cannot assign to readonly // Nested objects let user: { name: string; address: { street: string; city: string; }; } = { name: "Alice", address: { street: "123 Main St", city: "NYC" } }; // Index signature for dynamic keys let dictionary: { [key: string]: number } = { apples: 5, oranges: 3 }; dictionary["bananas"] = 7; // OK
Object Type Shape:
┌─────────────────────────────────────────┐
│ {                                       │
│   propertyName: Type;                   │
│   optionalProp?: Type;                  │
│   readonly fixedProp: Type;             │
│   [key: string]: Type;  // index sig    │
│ }                                       │
└─────────────────────────────────────────┘

Type Aliases

Type aliases create custom names for any type using the type keyword, improving code readability, enabling reuse, and simplifying complex type definitions. Unlike interfaces, type aliases can represent primitives, unions, tuples, and any other type; use them for unions and intersections, while interfaces are preferred for object shapes that might be extended.

// Basic type alias type UserID = string; type Age = number; // Object type alias type User = { id: UserID; name: string; age: Age; email?: string; }; // Using the alias const user: User = { id: "abc123", name: "Alice", age: 30 }; // Function type alias type Callback = (data: string) => void; type BinaryOp = (a: number, b: number) => number; const add: BinaryOp = (a, b) => a + b; // Tuple type alias type Coordinate = [number, number]; type RGB = [number, number, number]; const point: Coordinate = [10, 20]; const red: RGB = [255, 0, 0]; // Generic type alias type Container<T> = { value: T; timestamp: Date; }; const stringBox: Container<string> = { value: "hello", timestamp: new Date() };
Type Alias Pattern:
┌─────────────────────────────────────────┐
│  type AliasName = ExistingType;         │
│                                         │
│  type ID = string;                      │
│  type Point = { x: number; y: number }; │
│  type Pair<T> = [T, T];                 │
└─────────────────────────────────────────┘

Union Types

Union types allow a value to be one of several types, written with the pipe | operator between types. When working with union types, you can only access properties/methods common to all types unless you narrow the type first; unions are essential for modeling values that legitimately vary in type.

// Basic union let id: string | number; id = "abc123"; // OK id = 42; // OK // id = true; // Error: boolean not in union // Union in function parameter function printId(id: string | number): void { console.log(`ID: ${id}`); // Can only use common methods console.log(id.toString()); // OK - both have toString // Must narrow for specific methods if (typeof id === "string") { console.log(id.toUpperCase()); // OK after narrowing } else { console.log(id.toFixed(2)); // OK - must be number } } // Union with literals type Status = "pending" | "approved" | "rejected"; let orderStatus: Status = "pending"; // Union with type aliases type Cat = { meow: () => void }; type Dog = { bark: () => void }; type Pet = Cat | Dog; // Array of union types let mixed: (string | number)[] = [1, "two", 3, "four"]; // Nullable type (common pattern) type MaybeString = string | null | undefined;
Union Type:
┌─────────────────────────────────────────┐
│     Type A    |    Type B    |   ...    │
│       ↓              ↓                  │
│   Value can be ANY ONE of these types   │
│                                         │
│   string | number | boolean             │
│      ↓       ↓         ↓                │
│   "hello"   42       true               │
└─────────────────────────────────────────┘

Intersection Types

Intersection types combine multiple types into one using the & operator, requiring a value to satisfy all constituent types simultaneously. This is useful for composing types together, like mixing in shared properties; for objects, intersections merge all properties, while for primitives, impossible intersections become never.

// Combining object types type HasName = { name: string }; type HasAge = { age: number }; type HasEmail = { email: string }; type Person = HasName & HasAge; type ContactablePerson = Person & HasEmail; const user: ContactablePerson = { name: "Alice", age: 30, email: "alice@example.com" // Must have ALL properties }; // Mixin pattern type Timestamped = { createdAt: Date; updatedAt: Date }; type SoftDeletable = { deletedAt?: Date; isDeleted: boolean }; type Entity = { id: string; }; type FullEntity = Entity & Timestamped & SoftDeletable; // Intersection with function types type Logger = { log: (msg: string) => void }; type ErrorHandler = { handleError: (err: Error) => void }; type Service = Logger & ErrorHandler; const service: Service = { log: (msg) => console.log(msg), handleError: (err) => console.error(err) }; // Impossible intersection becomes never type Impossible = string & number; // never
Intersection vs Union:
┌─────────────────────────────────────────────────────┐
│  Union (|):        A  OR  B    →  one of them       │
│  Intersection (&): A  AND B    →  all of them       │
│                                                     │
│  type A = { a: string }                             │
│  type B = { b: number }                             │
│                                                     │
│  A | B  →  { a: string } OR { b: number }           │
│  A & B  →  { a: string, b: number }                 │
└─────────────────────────────────────────────────────┘

Literal Types

Literal types represent exact values rather than general types—a specific string, number, or boolean rather than any string, number, or boolean. Combined with unions, literal types create powerful patterns for defining allowed values; this is how TypeScript models enums without the enum keyword and enables discriminated unions.

// String literal types type Direction = "north" | "south" | "east" | "west"; let heading: Direction = "north"; // heading = "up"; // Error: not in union // Number literal types type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6; let roll: DiceRoll = 4; // roll = 7; // Error // Boolean literal (less common) type True = true; // Literal type from const const name = "Alice"; // Type: "Alice" (literal) let name2 = "Alice"; // Type: string (widened) // as const for literal inference const config = { endpoint: "/api", retries: 3 } as const; // Type: { readonly endpoint: "/api"; readonly retries: 3 } // Literal in function signature function setVolume(level: 0 | 25 | 50 | 75 | 100): void { console.log(`Volume set to ${level}%`); } setVolume(50); // OK // setVolume(30); // Error // Template literal types (TS 4.1+) type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"; type Endpoint = "/users" | "/posts"; type Route = `${HttpMethod} ${Endpoint}`; // "GET /users" | "GET /posts" | "POST /users" | ...
Type Widening:
┌─────────────────────────────────────────────────────┐
│  const x = "hello"  →  type is "hello" (literal)    │
│  let y = "hello"    →  type is string (widened)     │
│                                                     │
│  Literal: "hello"  ⊂  string  ⊂  any               │
│           (more specific → less specific)           │
└─────────────────────────────────────────────────────┘

Type Assertions

Type assertions tell TypeScript to treat a value as a specific type when you have more knowledge about the type than the compiler does, using either value as Type or <Type>value syntax (the former is preferred with JSX). Assertions don't perform runtime conversion—they only affect compile-time type checking, so use them carefully to avoid runtime errors.

// as syntax (preferred) const input = document.getElementById("myInput") as HTMLInputElement; input.value = "Hello"; // OK - we asserted it's an input // Angle bracket syntax (doesn't work in JSX) const element = <HTMLDivElement>document.querySelector(".container"); // Asserting unknown function processValue(val: unknown): void { const str = val as string; console.log(str.toUpperCase()); // Careful! Runtime error if not string } // Double assertion (escape hatch - avoid!) const x = "hello" as unknown as number; // Forces incompatible assertion // const assertion - makes everything readonly and literal const config = { endpoint: "/api", timeout: 3000 } as const; // Type: { readonly endpoint: "/api"; readonly timeout: 3000 } // Non-null assertion (!) function getElement(id: string): HTMLElement { const el = document.getElementById(id); return el!; // Assert it's not null (dangerous!) } // Better alternative - type guard function getElementSafe(id: string): HTMLElement { const el = document.getElementById(id); if (!el) throw new Error(`Element ${id} not found`); return el; // TypeScript knows it's not null here }
Assertion vs Casting:
┌─────────────────────────────────────────────────────┐
│  Type Assertion (TypeScript):                       │
│  - Compile-time only                                │
│  - No runtime conversion                            │
│  - "Trust me, I know the type"                      │
│                                                     │
│  Type Casting (other languages):                    │
│  - Runtime conversion                               │
│  - Actually transforms the value                    │
└─────────────────────────────────────────────────────┘

Type Narrowing and Type Guards

Type narrowing is TypeScript's ability to refine a type to a more specific type within a conditional block using type guards. Built-in guards include typeof for primitives, instanceof for classes, in for property existence, and truthiness checks; you can also create custom type guards using the paramName is Type return type.

// typeof narrowing (primitives) function padLeft(value: string | number, padding: string | number): string { if (typeof padding === "number") { return " ".repeat(padding) + value; // padding is number } return padding + value; // padding is string } // instanceof narrowing (classes) function processDate(date: Date | string): Date { if (date instanceof Date) { return date; // date is Date } return new Date(date); // date is string } // in narrowing (property existence) type Fish = { swim: () => void }; type Bird = { fly: () => void }; function move(animal: Fish | Bird): void { if ("swim" in animal) { animal.swim(); // animal is Fish } else { animal.fly(); // animal is Bird } } // Custom type guard function isString(value: unknown): value is string { return typeof value === "string"; } function process(val: unknown): void { if (isString(val)) { console.log(val.toUpperCase()); // val is string } } // Discriminated unions type Success = { status: "success"; data: string }; type Failure = { status: "failure"; error: Error }; type Result = Success | Failure; function handleResult(result: Result): void { if (result.status === "success") { console.log(result.data); // result is Success } else { console.log(result.error); // result is Failure } } // Truthiness narrowing function printName(name?: string | null): void { if (name) { console.log(name.toUpperCase()); // name is string } }
Narrowing Flow: ┌─────────────────────────────────────────────────────┐ │ param: string | number │ │ ↓ │ │ if (typeof param === "string") { │ │ ↓ │ │ param: string ← Narrowed! │ │ } else { │ │ param: number ← Also narrowed! │ │ } │ └─────────────────────────────────────────────────────┘ Type Guards: ┌────────────────┬────────────────────────────────────┐ │ typeof │ primitives (string, number, etc) │ │ instanceof │ class instances │ │ in │ property existence │ │ is (custom) │ complex conditions │ └────────────────┴────────────────────────────────────┘