TypeScript Fundamentals

TypeScript adds static types to JavaScript. Compiled to JS for execution; types are checked at compile time. Modern frontend development is largely TypeScript; node back-ends increasingly are too.

This page covers the practical TypeScript that matters day-to-day.

Why TypeScript

The case:

The cost:

For most non-trivial JavaScript projects, TypeScript pays for itself.

The basics

Types

let name: string = "Alice";
let age: number = 30;
let active: boolean = true;
let scores: number[] = [1, 2, 3];
let user: { name: string, age: number } = { name: "Bob", age: 25 };

Most of the time, you don't write the type — TypeScript infers it.

Functions

function greet(name: string): string {
    return `Hello, ${name}`;
}

const greet2 = (name: string): string => `Hello, ${name}`;

Parameters and return types specified; everything else inferred.

Interfaces and types

Two ways to declare object shapes:

interface User {
    id: string;
    name: string;
    email?: string;  // optional
}

type User = {
    id: string;
    name: string;
    email?: string;
};

Mostly interchangeable. Use whichever is conventional in your codebase.

Unions

type Status = 'pending' | 'active' | 'cancelled';

A value of Status can only be one of those strings. Compiler checks.

Generics

function first<T>(items: T[]): T | undefined {
    return items[0];
}

const x = first([1, 2, 3]);  // T inferred as number
const y = first(['a', 'b']); // T inferred as string

The <T> is a type parameter — fills in based on usage.

Structural typing

TypeScript types are structural, not nominal:

interface Point { x: number; y: number; }

const p1: Point = { x: 1, y: 2 };
const p2: { x: number, y: number, z: number } = { x: 1, y: 2, z: 3 };

function distance(p: Point) { /* ... */ }
distance(p1);  // OK
distance(p2);  // Also OK — has the required fields

The shape matters; the name doesn't. This is unlike Java/C# (nominal typing).

Common patterns

Type narrowing

function process(value: string | number) {
    if (typeof value === 'string') {
        // value is narrowed to string here
        return value.toUpperCase();
    }
    // value is narrowed to number here
    return value * 2;
}

The compiler tracks what type a variable has at each point.

Discriminated unions

type Result =
    | { kind: 'success', value: string }
    | { kind: 'error', error: string };

function handle(r: Result) {
    if (r.kind === 'success') {
        // r.value is accessible here
    } else {
        // r.error is accessible here
    }
}

The kind field discriminates between variants. Pattern matching emerges from narrowing.

Optional chaining and nullish coalescing

const value = obj?.prop?.nested ?? 'default';

?. returns undefined if anything in the chain is null/undefined. ?? provides a default for null/undefined (not for empty strings or zero).

Type assertions

const elem = document.getElementById('foo') as HTMLDivElement;

Tell the compiler "trust me, it's this type." Use sparingly; it bypasses type checking.

Utility types

TypeScript has built-in utility types:

Partial<User>     // all fields optional
Required<User>    // all fields required
Readonly<User>    // all fields readonly
Pick<User, 'id' | 'name'>  // just specific fields
Omit<User, 'email'>        // exclude fields

Useful for transforming types.

Strict mode

Enable strict checks in tsconfig.json:

{
    "compilerOptions": {
        "strict": true
    }
}

This enables: noImplicitAny, strictNullChecks, strictFunctionTypes, etc. The strict mode catches dramatically more bugs than the loose mode.

For new projects, always start with strict.

Adoption strategies

New project

Start with TypeScript and strict mode. No transition.

Existing JavaScript project

Two paths:

Big bang

Convert everything at once. Risky; usually delayed indefinitely.

Incremental

Add tsconfig.json with allowJs: true. Convert files one at a time. New files are TS; existing files stay JS until touched.

The incremental path is realistic. Some files may stay JS for years; that's fine.

any

any is TypeScript's escape hatch — anything goes. Useful in transition; should be avoided in new code.

For unknown values, use unknown instead of any. unknown requires you to narrow before use.

TypeScript and React

TypeScript works well with React:

type Props = {
    name: string;
    age?: number;
};

const Greeting: React.FC<Props> = ({ name, age }) => (
    <div>Hello, {name}, age {age ?? 'unknown'}</div>
);

// Or
function Greeting({ name, age }: Props) {
    return <div>Hello, {name}</div>;
}

The function-component declaration is increasingly preferred over React.FC.

What TypeScript doesn't fix

Common failure patterns

Further Reading