On this page

Primitive types and annotations

14 min read TextCh. 1 — TypeScript Fundamentals

The seven primitive types

TypeScript inherits JavaScript's seven primitive types and adds precise static typing on top of each one. A primitive is an immutable value that is not an object — it is stored directly, not by reference.

string

Represents text. TypeScript accepts single quotes, double quotes, and template literals — all have the same type.

const firstName: string = "Alice";
const lastName: string = 'Wonderland';
const fullName: string = `${firstName} ${lastName}`;

// Multiline template literal — still a string
const message: string = `
  Hello, ${firstName}!
  Welcome back.
`;

number

TypeScript has a single number type for both integers and floating-point values, matching JavaScript's IEEE 754 double-precision format.

const price: number = 9.99;
const quantity: number = 3;
const hex: number = 0xff;        // 255
const binary: number = 0b1010;   // 10
const octal: number = 0o17;      // 15
const large: number = 1_000_000; // numeric separators for readability

Be aware that number cannot represent integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1) without precision loss.

boolean

Only two values: true and false.

const isLoggedIn: boolean = false;
const hasPermission: boolean = true;

// Computed boolean — inference works perfectly here
const canEdit = isLoggedIn && hasPermission; // inferred as boolean

null and undefined

These are separate types in TypeScript with strict mode enabled. null means "intentionally absent value". undefined means "this variable has not been assigned yet".

// With strictNullChecks enabled (always on with strict: true):
let value: string = "hello";
value = null;      // Error: Type 'null' is not assignable to type 'string'
value = undefined; // Error: Type 'undefined' is not assignable to type 'string'

// Use a union to allow null or undefined explicitly
let nullable: string | null = "hello";
nullable = null; // OK

let optional: string | undefined = "world";
optional = undefined; // OK

bigint

The bigint type represents arbitrarily large integers, solving the precision problem of number for very large values. Bigint literals end with n.

const maxSafeInt: number = Number.MAX_SAFE_INTEGER;       // 9007199254740991
const beyondMax: bigint = 9_007_199_254_740_993n;          // exact

// Arithmetic works naturally with bigint
const a: bigint = 100n;
const b: bigint = 200n;
const sum: bigint = a + b; // 300n

// You cannot mix bigint and number without explicit conversion
const mixed = a + 1;   // Error: Cannot mix BigInt and other types
const converted = a + BigInt(1); // OK: 101n

symbol

Symbol() creates a unique, non-comparable identifier. No two symbols are ever equal, even if they have the same description.

const id1: symbol = Symbol("id");
const id2: symbol = Symbol("id");
console.log(id1 === id2); // false — always unique

// Symbols are often used as object keys to avoid collisions
const SERIALIZABLE = Symbol("serializable");

class DataModel {
  [SERIALIZABLE] = true;
}

Type annotations vs type inference

TypeScript can infer types automatically from values. You do not need to annotate every variable — in fact, redundant annotations are considered noise in modern TypeScript style guides.

// Redundant annotations — avoid these
const name: string = "Alice";
const count: number = 0;
const active: boolean = true;

// Preferred — let TypeScript infer from the value
const name = "Alice";   // inferred: string
const count = 0;        // inferred: number
const active = true;    // inferred: boolean

The rule of thumb: annotate at boundaries, infer in the middle.

Annotate:

  • Function parameters — TypeScript cannot infer these
  • Function return types — makes your API explicit and catches bugs early
  • Variables initialized to null or undefined
  • Variables declared without an initial value

Let TypeScript infer:

  • Local variables with obvious types
  • Intermediate computed values inside functions
  • Variables assigned from typed function return values
// Annotate: parameters and return type
function formatCurrency(amount: number, currency: string): string {
  return `${currency}${amount.toFixed(2)}`;
}

// Infer: the return value is clearly a string
const label = formatCurrency(9.99, "$"); // inferred: string

The any trap

any is an escape hatch that completely disables TypeScript's type checking for a value. It is sometimes necessary when working with untyped third-party code or during a migration, but it should be treated as a last resort, not a convenience.

let data: any = fetchFromExternalApi();

// TypeScript accepts every single one of these — no errors reported:
data.nonExistentMethod();   // will crash at runtime
data[0].anything;           // will crash at runtime
const result = data * "foo"; // produces NaN silently

The danger is not just that any bypasses checks on that variable — any also spreads to everything it touches. A function that accepts or returns any poisons the types of all its callers.

unknown: the safe alternative

unknown is the type-safe counterpart to any. A value of type unknown can hold anything, but TypeScript forces you to narrow its type before you use it.

function processInput(input: unknown): string {
  // TypeScript rejects this: Object is of type 'unknown'
  // return input.trim();

  // You must narrow first
  if (typeof input === "string") {
    return input.trim(); // here TypeScript knows input is string
  }
  if (typeof input === "number") {
    return input.toFixed(2);
  }
  return String(input);
}

Use unknown for:

  • JSON-parsed data where the structure is not guaranteed
  • Values from external APIs without TypeScript types
  • Generic utility functions that operate on arbitrary input
  • Error objects in catch blocks (useUnknownInCatchVariables is enabled by strict)

void and never

void is the return type for functions that do not return a value. It signals to callers that the return value should be ignored.

function logMessage(message: string): void {
  console.log(`[LOG] ${message}`);
  // No return statement needed — void functions implicitly return undefined
}

// void is also used in callback types
type EventHandler = (event: MouseEvent) => void;

never represents values that never occur. A function returning never either throws unconditionally, or loops forever. TypeScript also infers never in exhaustive checks.

// Function that always throws — return type is never
function fail(message: string): never {
  throw new Error(message);
}

// Function that never returns — return type is never
function infiniteLoop(): never {
  while (true) {
    processQueue();
  }
}

// never in exhaustive type checks
type Direction = "north" | "south" | "east" | "west";

function handleDirection(direction: Direction): string {
  switch (direction) {
    case "north": return "heading north";
    case "south": return "heading south";
    case "east":  return "heading east";
    case "west":  return "heading west";
    default: {
      // If you add a new Direction and forget to handle it here,
      // TypeScript reports an error on the next line
      const exhaustiveCheck: never = direction;
      return exhaustiveCheck;
    }
  }
}

The never check in the default branch is a powerful pattern: if you ever add a new direction without updating this switch, TypeScript will report a compile error immediately.

Practice

  1. Write a function safeParseJSON(raw: unknown): Record<string, unknown> that parses a JSON string. Use unknown for the parsed value, narrow it with type guards, and return a typed result.
  2. Create a variable declared as string | null and write a helper function that accepts it, handles the null case, and returns a non-null string. Observe how TypeScript narrows the type inside each branch.
  3. Implement an exhaustive switch for a union type with three members. Then add a fourth member to the union and confirm TypeScript reports an error at the default: never branch.

Avoid any at all costs
Using `any` defeats the entire purpose of TypeScript. It turns off all type checking for that variable, letting errors slip through silently. Use `unknown` when you genuinely do not know the type, then narrow it before use. Configure `noImplicitAny: true` (included in `strict`) to prevent TypeScript from inferring `any` automatically.
Annotate function signatures, let inference handle the rest
A good rule of thumb: always annotate function parameters and return types explicitly. For local variables, let TypeScript infer the type from the assigned value — the annotation would just be visual noise. Reserve explicit variable annotations for cases where the inferred type is too wide or you want to communicate intent.
void vs undefined
`void` means 'this function does not return a meaningful value'. It is subtly different from `undefined` — a function typed as `() => void` can technically return any value, but callers cannot use that value. Use `void` for callback types where the return value is ignored, and `undefined` for explicit 'no value' in data structures.
// The seven primitive types in TypeScript
const name: string = "Alice";
const age: number = 30;
const isActive: boolean = true;
const nothing: null = null;
const notSet: undefined = undefined;
const bigNumber: bigint = 9_007_199_254_740_993n;
const id: symbol = Symbol("userId");

// Type inference — no annotation needed when the value is obvious
const greeting = "Hello";   // inferred as string
const count = 42;           // inferred as number
const enabled = false;      // inferred as boolean