<< Home

Chapter 2: The Type System Fundamentals

TypeScript’s type system gives shape to your ideas so the compiler can reason about your code before it ever runs. This chapter introduces the core building blocks. You start with the simplest values, then see how TypeScript infers types, how different types can combine, and how to describe structures using aliases and interfaces. These foundations make larger programs steadier and far easier to maintain.

Primitive types, literals, and type annotations

Primitive types describe the simplest values in TypeScript. These include string, number, boolean, null, undefined, bigint, and symbol. When you add a type annotation to a variable you tell the compiler exactly what kind of data belongs there. Literal types add an extra layer by restricting a value to a specific string or number which can be useful for states or configuration.

let title: string = "TypeScript Guide";
let count: number = 5;
let ready: boolean = true;

let direction: "left" = "left";
let retryLimit: 3 = 3;

Literal types help the compiler narrow possibilities. A value with the type "left" cannot suddenly become "right". This clarity becomes powerful when building state machines or command systems because each branch becomes more predictable.

💡 Type annotations act like signposts. They tell both the compiler and future readers what your intention was at the moment you wrote the code.

Type inference and best practices

TypeScript often knows the type of a value without any annotation. This behaviour is called type inference. If you assign a string literal to a variable the compiler recognises it as a string. If you initialise an array of numbers the compiler tracks that pattern automatically. This reduces clutter and keeps your code readable while still giving you the safety of static types.

let username = "robin";     // inferred as string
let scores = [10, 15, 20];  // inferred as number[]
let active = false;         // inferred as boolean

Inference works best when variables are declared close to where they are used. It is safer to add explicit annotations when values begin empty or when the intent is not immediately clear. For example a variable intended to hold a number later should not begin as null without an annotation because inference would fix its type too narrowly.

⚠️ Avoid annotating everything. Too many annotations make code feel heavy. Instead annotate only when necessary so inference can carry part of the load.

Union, intersection, and nullable types

Union types describe values that may take one form or another. This is useful when a function may accept more than one shape. Intersections work in the opposite direction by combining multiple types into one. Nullable types are simply unions that include null or undefined. These patterns let you express real world flexibility while keeping your rules steady.

let value: string | number = "start";
value = 42;

type WithId = { id: number };
type WithName = { name: string };
type User = WithId & WithName;

let user: User = { id: 1, name: "Robin" };

let maybe: string | null = null;

Union types help you model behaviour that branches based on conditions. Intersection types help merge independent structures into a single coherent shape. Nullable types remind you to check for missing values which avoids runtime surprises.

💡 If you find yourself writing many condition checks for null, consider narrowing early with guard functions so later code paths stay clean.

Type aliases and interfaces

Type aliases and interfaces both describe object shapes. A type alias assigns a name to any kind of type including unions, intersections, and primitives. An interface describes the structure of an object and can be extended by other interfaces. Interfaces communicate intent clearly and type aliases offer flexibility for complex combinations. Both approaches give the compiler a clear contract to enforce.

type Point = {
  x: number;
  y: number;
};

interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  id: number;
}

Interfaces are often preferred for modelling objects because they support extension. Type aliases are ideal when building more advanced types such as unions or template literal types. You can choose the form that best matches your design and mix them freely as your project grows.

⚠️ Avoid building huge all purpose interfaces. Smaller focused shapes combine more easily and keep each part of your code easier to maintain.