TypeScript Fundamentals - Types & Type System

The Production Bug That Taught Me Type Safety

Three months into using TypeScript, I thought I understood types. Primitives, arrays, objects - simple stuff. Then I merged a seemingly innocent PR:

interface Config {
    port: number;
    timeout: number;
    retries: number;
}

function startServer(config: Config) {
    server.listen(config.port);
    http.setTimeout(config.timeout);
    http.setMaxRetries(config.retries);
}

// Load from environment
const config: Config = {
    port: process.env.PORT,        // ← Looks fine to TypeScript!
    timeout: process.env.TIMEOUT,
    retries: process.env.RETRIES
};

startServer(config);

TypeScript compiled without errors. Tests passed. Deployed to production. Within 5 minutes, our API was down.

The problem? process.env.PORT returns a string, not a number. JavaScript's type coercion made server.listen("8080") work locally, but our load balancer expected a numeric port. The server was listening on port 0 (failed coercion) and became unreachable.

The fix:

The lesson: TypeScript's type system is powerful, but you must understand how types work to use it effectively. process.env returns string | undefined, but I assumed it would enforce number.

This article covers everything you need to know about TypeScript's type system to avoid mistakes like mine.

Basic Types

TypeScript provides several primitive types that correspond to JavaScript primitives.

String

Number

TypeScript has one number type for all numeric values (integers, floats, hex, binary):

Boolean

Null and Undefined

Any

The any type disables type checking - use sparingly:

When to use any:

  • Migrating JavaScript to TypeScript incrementally

  • Working with truly dynamic data (rare)

  • Third-party libraries without types (use unknown instead if possible)

Unknown

The type-safe alternative to any - requires type checking before use:

Rule of thumb: Prefer unknown over any when you don't know the type.

Void

Used for functions that don't return a value:

Never

Represents values that never occur:

Arrays

Array Type Syntax

Array Methods Preserve Types

ReadonlyArray

Prevents array mutation:

Tuples

Fixed-length arrays with specific types for each position:

Named Tuples (TypeScript 4.0+)

Type Annotations vs Type Inference

TypeScript can often infer types without explicit annotations.

Type Inference

When to Use Explicit Annotations

1. Function parameters (always annotate):

2. Variables without initialization:

3. Complex types:

Literal Types

Values themselves can be types:

Combining Literals

Type Assertions

Tell TypeScript "trust me, I know what I'm doing" (use carefully):

Warning: Type assertions don't perform runtime checks. Use only when you know more than TypeScript.

Real-World Example: Type-Safe Configuration

Here's how to fix my production bug from the opening story:

Before (Unsafe)

After (Type-Safe)

Your Challenge

Build a type-safe user validation system:

Requirements:

  1. Create a User type with:

    • id: number

    • username: string (3-20 characters)

    • email: string

    • role: "admin" | "user" | "guest"

    • isActive: boolean

    • lastLogin: Date | null

  2. Create a validation function that:

    • Takes unknown input

    • Validates each field

    • Returns a typed User or throws an error

  3. Handle edge cases:

    • Missing fields

    • Wrong types

    • Invalid role values

    • Null vs undefined

Starter code:

Key Takeaways

  1. Use strict mode - Enable strictNullChecks and noImplicitAny in tsconfig.json

  2. Prefer unknown over any - Unknown is type-safe, any is not

  3. Trust type inference - Don't over-annotate, let TypeScript infer when obvious

  4. Annotate function parameters - Always specify parameter types

  5. Use literal types for enums - Better than magic strings

  6. Validate external data - Types don't protect against runtime input

  7. Be careful with assertions - They bypass type safety

What I Learned From the Environment Variable Bug

  1. process.env is always strings - Never assume numeric types

  2. Type assertions are dangerous - Only use when you're certain

  3. Validate at boundaries - External inputs need runtime validation

  4. Compile-time β‰  runtime - Types disappear after compilation

  5. Default values are essential - Always handle undefined

The type system is your first line of defense, but it's not magic. Understanding how types work - and their limitations - is essential for writing robust TypeScript code.


Next: Interfaces and Type Aliases - Dive into object types, when to use interfaces vs type aliases, and designing type-safe APIs.

Last updated