> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript Fundamentals

> Types, Type Inference, and Basic Annotations

<img src="https://mintcdn.com/devweeekends/CHfRzoAmD5TGW2ch/images/courses/typescript-crash-course/type-system.svg?fit=max&auto=format&n=CHfRzoAmD5TGW2ch&q=85&s=cee3fc66fb4bc7b49a6fa0d6d8207569" alt="TypeScript Type System" width="1080" height="1080" data-path="images/courses/typescript-crash-course/type-system.svg" />

# TypeScript Fundamentals

TypeScript adds a **static type system** on top of JavaScript. Understanding types is the foundation of everything else in TypeScript. If you skip this chapter, nothing else will make sense -- types are to TypeScript what notes are to music theory.

***

## How TypeScript Works: Transpilation

TypeScript is **not** directly executed. Browsers and Node.js only understand JavaScript. TypeScript must be **transpiled** (compiled) to JavaScript before it can run. Think of TypeScript as writing in a markup language (like Markdown) -- the final consumer (the browser) never sees the markup, only the rendered output (JavaScript). The types are scaffolding that helps you build correctly, then gets removed from the finished building.

### The TypeScript Compilation Pipeline

```mermaid theme={null}
graph LR
    A[Source .ts/.tsx] -->|Lexer| B[Tokens]
    B -->|Parser| C[AST]
    C -->|Type Checker| D[Typed AST]
    D -->|Emitter| E[JavaScript .js]
    E -->|Runtime| F[Execution]
    
    style A fill:#3178c6,stroke:#235a97,color:#fff
    style E fill:#f7df1e,stroke:#c9b516,color:#000
```

| Stage             | What Happens                                | Can You See It?         |
| ----------------- | ------------------------------------------- | ----------------------- |
| **Lexing**        | Source code is broken into tokens           | Internal                |
| **Parsing**       | Tokens become an Abstract Syntax Tree (AST) | `ts.createSourceFile()` |
| **Type Checking** | Types are validated, errors reported        | Your IDE red squiggles! |
| **Emitting**      | AST is transformed to JavaScript            | The `.js` files         |

### Type Erasure

Here is the key insight that many beginners miss: **Types exist only at compile time**. They are completely erased in the output JavaScript. This means TypeScript has zero runtime cost -- no performance penalty, no extra bytes shipped to users. But it also means you cannot use TypeScript types for runtime decisions (more on that below).

```typescript theme={null}
// TypeScript (input)
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const user: { id: number; name: string } = {
  id: 1,
  name: 'Alice'
};
```

```javascript theme={null}
// JavaScript (output) - Types are gone!
function greet(name) {
  return `Hello, ${name}!`;
}

const user = {
  id: 1,
  name: 'Alice'
};
```

<Warning>
  **Runtime Implications**: Since types are erased, you cannot use TypeScript types for runtime checks. You can't do `if (typeof x === "string")` based on a TypeScript `string` type—that's just JavaScript's `typeof`.
</Warning>

### The TypeScript Compiler (tsc)

The `tsc` command is the TypeScript compiler:

```bash theme={null}
# Compile a single file
tsc hello.ts          # Outputs hello.js

# Compile with options
tsc --target ES2020 --module ESNext hello.ts

# Watch mode (recompile on changes)
tsc --watch

# Initialize a new project with tsconfig.json
tsc --init

# Compile entire project (uses tsconfig.json)
tsc
```

### Compile Time vs Runtime Errors

| Error Type       | When Caught      | Example                           |
| ---------------- | ---------------- | --------------------------------- |
| **Compile-time** | Before code runs | Type mismatch, missing properties |
| **Runtime**      | While code runs  | Network failures, user input      |

```typescript theme={null}
// Compile-time error (caught by TypeScript)
let name: string = 42; // ❌ Type 'number' is not assignable to 'string'

// Runtime error (NOT caught by TypeScript)
const data = JSON.parse(userInput); // Might throw if invalid JSON
```

<Tip>
  **Why This Matters**: TypeScript's value is catching bugs BEFORE your code runs. But it can't protect you from external data (APIs, user input)—you still need runtime validation for that.
</Tip>

***

## 1. Type Annotations

Type annotations explicitly declare the type of a variable, parameter, or return value.

```typescript theme={null}
// Variable annotations
let name: string = 'Alice';
let age: number = 25;
let isActive: boolean = true;

// The annotation is after the variable name, separated by :
let count: number;  // Declared but not initialized
count = 42;         // Assigned later
```

### Why Annotate?

```typescript theme={null}
let name: string = 'Alice';
name = 42; // ❌ Error: Type 'number' is not assignable to type 'string'
```

TypeScript catches type mismatches at **compile time**, not runtime.

***

## 2. Type Inference

TypeScript is smart. It can **infer types** from the value you assign -- meaning you often do not need to write types at all. The compiler looks at the right-hand side of the assignment and figures out the type automatically. This is what makes TypeScript feel lightweight instead of verbose like Java.

```typescript theme={null}
// TypeScript infers these types automatically
let name = 'Alice';     // string (inferred)
let age = 25;           // number (inferred)
let isActive = true;    // boolean (inferred)

name = 42; // ❌ Error: Still type 'string'!
```

<Tip>
  **Best Practice**: Let TypeScript infer types when the type is obvious from the value. Add explicit annotations when:

  * The type isn't obvious
  * You're declaring without initializing
  * You want to be explicit for documentation
</Tip>

### When to Annotate vs Infer

```typescript theme={null}
// Let TypeScript infer (cleaner)
const user = { name: 'Alice', age: 25 };

// Explicit annotation (more control)
const user: { name: string; age: number } = { name: 'Alice', age: 25 };

// Always annotate function parameters
function greet(name: string): string {
  return `Hello, ${name}!`;
}
```

***

## 3. Primitive Types

TypeScript has the same primitive types as JavaScript, plus a few extras.

### Basic Primitives

```typescript theme={null}
// string
let firstName: string = 'Alice';
let greeting: string = `Hello, ${firstName}`;

// number (integers and floats)
let age: number = 25;
let price: number = 19.99;
let hex: number = 0xff;
let binary: number = 0b1010;

// boolean
let isLoggedIn: boolean = true;
let hasAccess: boolean = false;

// null and undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// symbol
let id: symbol = Symbol('id');

// bigint
let bigNumber: bigint = 9007199254740993n;
```

### Special Types

```typescript theme={null}
// any - Opt out of type checking entirely (avoid in production code!)
// Using 'any' is like disabling your seatbelt -- it removes all protection.
let anything: any = 'hello';
anything = 42;        // No error -- TypeScript stops checking
anything = { foo: 'bar' }; // Anything goes

// unknown - The type-safe alternative to any.
// Think of 'unknown' as a locked box: you know something is inside,
// but you must prove what it is before you can use it.
let value: unknown = 'hello';
// value.toUpperCase(); // Error: must narrow the type first
if (typeof value === 'string') {
  console.log(value.toUpperCase()); // OK -- TypeScript now knows it is a string
}

// void - Function returns nothing meaningful
function log(message: string): void {
  console.log(message);
  // Implicitly returns undefined, which is fine for void
}

// never - Function NEVER returns (throws an error or runs forever)
// This is different from void: void returns undefined, never does not return at all.
function throwError(message: string): never {
  throw new Error(message);
}
```

<Warning>
  **Avoid `any`!** It defeats the purpose of TypeScript. Use `unknown` if you truly don't know the type, then narrow it with type guards.
</Warning>

***

## 4. Arrays

Arrays can be typed in two ways:

```typescript theme={null}
// Using Type[]
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ['Alice', 'Bob', 'Charlie'];

// Using Array<Type> (generic syntax)
let scores: Array<number> = [100, 95, 88];

// Mixed types? Use union
let mixed: (string | number)[] = [1, 'two', 3, 'four'];

// Readonly arrays
const readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers.push(4); // ❌ Error: Property 'push' does not exist
```

### Array Methods with Types

```typescript theme={null}
const numbers = [1, 2, 3, 4, 5];

// map - TypeScript infers the return type
const doubled = numbers.map(n => n * 2); // number[]

// filter - Returns same type
const evens = numbers.filter(n => n % 2 === 0); // number[]

// find - Returns Type | undefined
const found = numbers.find(n => n > 3); // number | undefined

// reduce - Explicit accumulator type
const sum = numbers.reduce((acc, n) => acc + n, 0); // number
```

***

## 5. Tuples

Tuples are fixed-length arrays with specific types at each position. While a regular array says "this is a list of strings," a tuple says "this is exactly a string, then a number, then a boolean, in that order." They are useful for representing structured data without creating a full interface -- like function return values that pack multiple pieces of information together.

```typescript theme={null}
// Define a tuple
let person: [string, number] = ['Alice', 25];

// Access elements (typed!)
const name = person[0]; // string
const age = person[1];  // number

// Error on wrong type
person[0] = 42; // ❌ Error: Type 'number' is not assignable to type 'string'

// Error on extra elements (in strict mode)
person = ['Bob', 30, true]; // ❌ Error: Source has 3 elements but target allows only 2
```

### Labeled Tuples (TS 4.0+)

```typescript theme={null}
type UserTuple = [name: string, age: number, isAdmin: boolean];

const user: UserTuple = ['Alice', 25, true];

// Labels are just for documentation, access is still by index
const name = user[0]; // string
```

### Optional Tuple Elements

```typescript theme={null}
type Response = [number, string, object?];

const success: Response = [200, 'OK'];
const withData: Response = [200, 'OK', { user: 'Alice' }];
```

***

## 6. Enums

Enums define a set of named constants.

### Numeric Enums

```typescript theme={null}
enum Direction {
  Up,     // 0
  Down,   // 1
  Left,   // 2
  Right   // 3
}

let move: Direction = Direction.Up;
console.log(move);             // 0
console.log(Direction[0]);     // 'Up' (reverse mapping)

// Custom values
enum Status {
  Pending = 1,
  Active = 2,
  Inactive = 3
}
```

### String Enums

```typescript theme={null}
enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE'
}

let favorite: Color = Color.Blue;
console.log(favorite); // 'BLUE'
```

### const Enums (Inlined at compile time)

```typescript theme={null}
const enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

const status = HttpStatus.OK; // Compiled to: const status = 200;
```

<Tip>
  **Modern Alternative**: Many developers prefer union types over enums for better tree-shaking and simpler code:

  ```typescript theme={null}
  type Direction = 'up' | 'down' | 'left' | 'right';
  ```
</Tip>

***

## 7. Object Types

Define the shape of objects with inline types or type aliases.

### Inline Object Types

```typescript theme={null}
// Inline type annotation
let user: { name: string; age: number } = {
  name: 'Alice',
  age: 25
};

// Optional properties with ?
let config: { debug?: boolean; timeout: number } = {
  timeout: 3000
  // debug is optional
};

// Readonly properties
let point: { readonly x: number; readonly y: number } = { x: 10, y: 20 };
// point.x = 5; // ❌ Error: Cannot assign to 'x' because it is a read-only property
```

### Type Aliases

Create reusable type definitions.

```typescript theme={null}
type User = {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
};

const alice: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  isActive: true
};

const bob: User = {
  id: 2,
  name: 'Bob',
  email: 'bob@example.com',
  isActive: false
};
```

***

## 8. Union Types

A value can be one of several types.

```typescript theme={null}
// Basic union
let id: string | number;
id = 'abc123';
id = 123;

// Function with union parameter
function printId(id: string | number): void {
  console.log(`ID: ${id}`);
}

printId('abc');
printId(123);
```

### Narrowing Union Types

```typescript theme={null}
function printId(id: string | number): void {
  if (typeof id === 'string') {
    // TypeScript knows id is string here
    console.log(id.toUpperCase());
  } else {
    // TypeScript knows id is number here
    console.log(id.toFixed(2));
  }
}
```

### Literal Types

```typescript theme={null}
// Specific string values
type Status = 'pending' | 'active' | 'inactive';

let userStatus: Status = 'active';
// userStatus = 'unknown'; // ❌ Error: Type '"unknown"' is not assignable

// Specific numbers
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function roll(): DiceRoll {
  return Math.ceil(Math.random() * 6) as DiceRoll;
}
```

***

## 9. Type Assertions

Tell TypeScript you know better about the type. Assertions do not change the runtime value -- they only override the compiler's type analysis. Think of it as telling the compiler "trust me, I know what this is" rather than actually converting anything.

```typescript theme={null}
// as syntax (preferred) -- "I know this element is specifically an input, not just any element"
const input = document.getElementById('username') as HTMLInputElement;
input.value = 'Alice'; // Now TypeScript allows .value access

// Angle bracket syntax (not usable in JSX/TSX files because it conflicts with HTML tags)
const input2 = <HTMLInputElement>document.getElementById('password');

// Double assertion (escape hatch -- bypasses type safety completely)
// You almost never need this. If you do, it usually means your types are wrong.
const x = 'hello' as unknown as number; // Dangerous: no runtime conversion happens!
```

### Non-null Assertion

```typescript theme={null}
function getLength(value: string | null): number {
  // Tell TypeScript value is definitely not null
  return value!.length; // ⚠️ Use only when you're certain
}

// Better approach: actually check
function getLengthSafe(value: string | null): number {
  if (value === null) {
    return 0;
  }
  return value.length;
}
```

<Warning>
  **Use assertions sparingly!** They override TypeScript's type checking. If you're wrong, you'll get runtime errors. Prefer type guards for safety.
</Warning>

***

## 10. Type Narrowing

Type narrowing is how you go from a broad type to a specific one inside a code block. TypeScript's control flow analysis tracks which branches you have taken and automatically narrows the type -- this is one of the most powerful and practical features of the type system.

### typeof Guard

```typescript theme={null}
function process(value: string | number) {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value * 2;
}
```

### Truthiness Narrowing

```typescript theme={null}
function printName(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase()); // name is string
  } else {
    console.log('No name provided');
  }
}
```

### instanceof Guard

```typescript theme={null}
function logDate(date: Date | string) {
  if (date instanceof Date) {
    console.log(date.toISOString());
  } else {
    console.log(new Date(date).toISOString());
  }
}
```

### in Operator

```typescript theme={null}
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim();
  } else {
    animal.fly();
  }
}
```

***

## Summary

| Concept         | Example                                 |
| :-------------- | :-------------------------------------- |
| Type Annotation | `let name: string = 'Alice'`            |
| Type Inference  | `let age = 25` (inferred as number)     |
| Arrays          | `let nums: number[] = [1, 2, 3]`        |
| Tuples          | `let pair: [string, number] = ['a', 1]` |
| Union Types     | `let id: string \| number`              |
| Literal Types   | `type Status = 'on' \| 'off'`           |
| Type Aliases    | `type User = { name: string }`          |
| Type Assertions | `value as string`                       |
| Type Guards     | `typeof`, `instanceof`, `in`            |

Next, we'll explore functions and how TypeScript makes them safer and more expressive!

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="Q: Explain the difference between 'any', 'unknown', and 'never' in TypeScript. When would you use each, and what are the dangers of each?">
    **Strong Answer:**

    These three types represent the extremes of TypeScript's type system, and confusing them is a common interview red flag.

    * **`any`**: The escape hatch. It disables all type checking for that value -- you can call any method, access any property, assign it to anything. Think of it as telling the compiler "stop looking at this." It is dangerous because errors that TypeScript would normally catch at compile time become runtime exceptions. The only legitimate uses are: migrating a large JavaScript codebase incrementally (temporarily marking untyped code as `any`), and interacting with truly dynamic third-party code where writing types is impractical. In production TypeScript, having `any` in your codebase is technical debt.
    * **`unknown`**: The type-safe alternative to `any`. It says "this value could be anything, but you must prove what it is before you use it." You cannot access properties, call methods, or assign `unknown` to a typed variable without narrowing first (via `typeof`, `instanceof`, or a custom type guard). This is the correct type for external data: API responses, `JSON.parse()` output, user input, `catch` block errors. You accept the data as `unknown`, validate its shape, and only then work with it as a typed value.
    * **`never`**: Represents values that can never occur. A function that always throws returns `never`. A variable in the `default` branch of an exhaustive switch is `never` (if you handled all cases, no value can reach here). The most powerful use of `never` is exhaustiveness checking: `const _exhaustive: never = shape;` in a switch default forces a compile error if you add a new union variant without handling it.

    The mental model: `any` is "I do not care about types," `unknown` is "I do not know the type yet but I will find out," and `never` is "this should be impossible."

    **Follow-up: Show me how 'never' is used for exhaustiveness checking in a discriminated union.**

    Consider `type Action = { type: 'add' } | { type: 'delete' } | { type: 'update' }` and a `switch` on `action.type`. If you handle all three cases, the default branch's `action` is narrowed to `never` -- no value can reach it. Assigning `const _: never = action` compiles fine. Now if someone adds `{ type: 'archive' }` to the union but forgets to add a case, the default branch sees `action` as `{ type: 'archive' }`, which is not assignable to `never`, and the compiler errors immediately. This turns a potential runtime bug into a compile-time error, which is exactly the value proposition of TypeScript.
  </Accordion>

  <Accordion title="Q: What is type erasure and why does it mean you cannot use TypeScript interfaces for runtime type checking?">
    **Strong Answer:**

    Type erasure is the fundamental architectural decision of TypeScript: all type annotations, interfaces, type aliases, and generic type parameters are removed during compilation. The output JavaScript has zero traces of the type system.

    * **What this means**: `interface User { name: string; email: string }` does not exist at runtime. There is no `User` object, no `User` class, no `User` symbol in the compiled JavaScript. If you write `if (x instanceof User)`, it is a compile error because `User` is not a value -- it is a type that was erased.
    * **Why this matters**: You cannot validate incoming data against a TypeScript interface at runtime. `JSON.parse(apiResponse)` returns `any` (or `unknown` if you are careful), and no amount of TypeScript types can verify that the parsed object actually matches your `User` interface. The JSON could contain `{ name: 42, email: null }` and TypeScript would not catch it because the type system is gone at runtime.
    * **The solution**: Runtime validation libraries (Zod, io-ts, Yup) or custom type guard functions. Zod, for example, lets you define a schema that serves as both a runtime validator and a TypeScript type: `const UserSchema = z.object({ name: z.string(), email: z.string().email() }); type User = z.infer<typeof UserSchema>;`. Now `UserSchema.parse(data)` validates at runtime, and `User` provides compile-time types -- single source of truth.
    * **Classes are the exception**: TypeScript classes compile to JavaScript classes, so `instanceof` works on classes. This is why some teams prefer classes over interfaces for domain entities that need runtime validation.

    **Follow-up: How does type erasure affect generic types at runtime?**

    Generic type parameters are completely erased. `function identity<T>(value: T): T` compiles to `function identity(value) { return value; }` -- there is no way to know at runtime what `T` was. You cannot write `if (T === string)` inside the function because `T` does not exist at runtime. If you need different behavior based on the type, you must pass a runtime discriminator (an explicit string tag, a class constructor, or a type guard function) alongside the value.
  </Accordion>

  <Accordion title="Q: When should you rely on type inference versus writing explicit type annotations? What is the professional rule of thumb?">
    **Strong Answer:**

    The rule I follow is: let TypeScript infer when the type is obvious from the assignment, annotate when it is not obvious or when you are defining a contract boundary.

    * **Infer for local variables**: `const name = 'Alice'` -- the type `string` is obvious. Adding `: string` is visual noise that makes the code harder to scan. Same for `const users = [{ id: 1, name: 'Alice' }]` -- TypeScript infers the full array type correctly.
    * **Annotate function parameters always**: `function greet(name)` gives `name` the type `any` (with strict mode, it is an error). Function parameters are contract boundaries -- the caller needs to know what to pass. `function greet(name: string)` is a contract.
    * **Annotate function return types for public APIs**: For exported functions, library functions, or anything that forms a module boundary, explicit return types serve as documentation and prevent accidental type changes. If you refactor the function body and accidentally change the return type, an explicit annotation catches it. For internal helper functions, return type inference is usually fine.
    * **Annotate when inference gets it wrong**: `const status = 'active'` infers the literal type `'active'`, not `string`. If you want `string`, you annotate. Conversely, `let status = 'active'` infers `string` (because `let` can be reassigned). If you want the literal type, use `as const` or annotate.
    * **Annotate empty initializations**: `const items = []` infers `any[]`. You must annotate: `const items: string[] = []`.

    The key insight is that inference and annotation serve different purposes. Inference reduces verbosity for the code author. Annotations enforce contracts at boundaries for the code consumer. The best TypeScript codebases use inference liberally inside functions and annotations strictly at function signatures and module boundaries.

    **Follow-up: What happens when TypeScript infers a type that is more specific than you want?**

    This comes up with `const` declarations and object literals. `const config = { timeout: 3000 }` infers `{ timeout: number }`, which is fine. But `const method = 'GET'` infers the literal type `'GET'`, not `string`. If you pass it to a function expecting `string`, it works (literals are subtypes of string). But if a function returns a string and you compare it to `method`, TypeScript may warn about comparing `string` to `'GET'`. The tools are: `as const` to make types narrower (literal), explicit annotation to make types wider (general), and `satisfies` (TypeScript 4.9+) to validate a type without widening it.
  </Accordion>

  <Accordion title="Q: Explain how TypeScript's control flow narrowing works. What happens when you write an 'if (typeof x === string)' check?">
    **Strong Answer:**

    Control flow narrowing is one of TypeScript's most sophisticated features, and it is what makes union types practical rather than painful.

    * **The mechanism**: TypeScript's type checker tracks the type of every variable through every branch of your code. When you write `if (typeof x === 'string')`, TypeScript narrows the type of `x` to `string` inside the `if` block and to "whatever was left" in the `else` block. If `x` was `string | number`, it becomes `string` in the `if` and `number` in the `else`.
    * **What triggers narrowing**: `typeof` checks, `instanceof` checks, `in` operator (`'swim' in animal`), equality checks (`x === null`), truthiness checks (`if (x)`), and custom type guards (`function isFish(x): x is Fish`). TypeScript also narrows on assignment: if you assign a string to a `string | number` variable, the type narrows to `string` after the assignment.
    * **Control flow analysis**: TypeScript follows the actual control flow, not just the immediate block. If you check `if (x === null) return;`, TypeScript knows that after the return, `x` is not null for the rest of the function. This works with `throw`, `return`, `break`, and `continue` -- any statement that makes a code path unreachable narrows the types for the remaining paths.
    * **The limitation**: Narrowing is per-reference, not per-value. If you narrow `obj.value` to `string` inside an `if` block, TypeScript narrows it for that block. But if you call a function in between that could mutate `obj`, TypeScript may widen the type back because it cannot guarantee the function did not change `obj.value`. This is why storing narrowed values in local constants (`const val = obj.value; if (typeof val === 'string') ...`) is a common pattern -- local constants cannot be mutated by external calls.

    **Follow-up: How do custom type guard functions work, and why are they sometimes better than inline typeof checks?**

    A custom type guard is a function whose return type is a type predicate: `function isUser(data: unknown): data is User`. When this function returns `true`, TypeScript narrows the argument to `User` in the calling scope. The advantage over inline checks is reusability and encapsulation. Validating that an API response is a `User` requires checking multiple properties (`typeof data.name === 'string' && typeof data.id === 'number' && ...`). Inlining that check everywhere is verbose and error-prone. Encapsulating it in `isUser()` means you write the validation once, test it once, and use it everywhere. TypeScript propagates the narrowing wherever you call it.
  </Accordion>
</AccordionGroup>
