On this page

Arrays, tuples, and enums

14 min read TextCh. 2 — Compound Types

Typed arrays

Arrays in TypeScript are typed collections where every element has the same type. TypeScript provides two syntaxes for declaring array types — they are completely equivalent, and which you use is a matter of style:

const scores: number[] = [100, 87, 95];       // shorthand syntax
const names: Array<string> = ["Alice", "Bob"]; // generic syntax

The generic syntax Array<T> looks more consistent with other generic types. The shorthand T[] is more concise and is preferred by most style guides. This course uses the shorthand.

Readonly arrays

Arrays are mutable by default. If you want to prevent modification — an essential technique for working with configuration, constants, or data you pass to external systems — use readonly:

const SUPPORTED_LOCALES: readonly string[] = ["en", "es", "fr", "de"];

// All of these are compile errors:
SUPPORTED_LOCALES.push("ja");     // cannot add
SUPPORTED_LOCALES.pop();          // cannot remove
SUPPORTED_LOCALES[0] = "zh";      // cannot modify
SUPPORTED_LOCALES.sort();         // cannot sort (mutates in place)

// Reading is always allowed:
const first = SUPPORTED_LOCALES[0]; // "en"
const count = SUPPORTED_LOCALES.length; // 4

ReadonlyArray<T> is the long form and is identical in behaviour to readonly T[].

Typed arrays in practice

When you iterate or map over a typed array, TypeScript infers the element type automatically:

const prices: number[] = [9.99, 14.99, 4.99];

// item is inferred as number — no annotation needed
const discounted = prices.map((item) => item * 0.9);

// TypeScript catches invalid operations on the element type
const lengths = prices.map((item) => item.toUpperCase()); // Error: 'toUpperCase' does not exist on 'number'

Tuples

A tuple is a fixed-length array where each position has its own specific type. Where an array says "a list of values all of the same type", a tuple says "exactly N values, each with a defined type at a specific index".

// A coordinate pair: [x, y]
type Point2D = [number, number];
const origin: Point2D = [0, 0];
const cursor: Point2D = [42, 17];

// Length and types are enforced
const invalid: Point2D = [0, 0, 0];    // Error: Source has 3 element(s) but target allows only 2
const alsoInvalid: Point2D = [0, "y"]; // Error: Type 'string' is not assignable to type 'number'

Named tuples

Named tuples label each element without changing the type. They make tuple types self-documenting and improve the hover text in editors:

type DateRange = [start: Date, end: Date];
type RGBColor = [red: number, green: number, blue: number];

const range: DateRange = [new Date("2026-01-01"), new Date("2026-12-31")];
const white: RGBColor = [255, 255, 255];

// Destructuring uses the declared names as hints (optional)
const [start, end] = range;

Tuple use cases

Tuples are commonly used for:

  • Return values from functions where a specific structure is expected
  • React-style hook patterns that return [value, setter]
  • CSV/table row data where each column has a known type
// Hook-style function returning a tuple
function useCounter(initial: number): [count: number, increment: () => void, reset: () => void] {
  let count = initial;
  return [
    count,
    () => { count++; },
    () => { count = initial; },
  ];
}

const [count, increment, reset] = useCounter(0);

Optional and rest elements in tuples

Tuples can have optional trailing elements (marked with ?) and rest elements:

// Optional last element
type Pair = [string, string?];
const solo: Pair = ["alone"];             // second element omitted
const duo: Pair = ["first", "second"];

// Rest element — zero or more strings after the first number
type LogEntry = [timestamp: number, ...messages: string[]];
const entry: LogEntry = [1712345678, "Server started", "Listening on port 3000"];

Enums

Enums define a named set of constants. They are the one TypeScript feature that has no direct JavaScript equivalent — enums generate real JavaScript code.

String enums

String enums give each member an explicit string value. They are the most common and safest kind of enum:

enum LogLevel {
  Debug   = "DEBUG",
  Info    = "INFO",
  Warning = "WARNING",
  Error   = "ERROR",
}

function log(level: LogLevel, message: string): void {
  console.log(`[${level}] ${message}`);
}

log(LogLevel.Info, "Server started");   // [INFO] Server started
log("INFO", "Server started");          // Error: Argument of type '"INFO"' is not assignable to parameter of type 'LogLevel'

The last line is an important safety feature: TypeScript refuses to accept a bare string even if its value matches an enum member. You must use the enum reference.

Numeric enums

Numeric enums assign integer values starting from 0 by default. You can override the starting value:

enum HttpStatus {
  Ok                  = 200,
  Created             = 201,
  BadRequest          = 400,
  Unauthorized        = 401,
  NotFound            = 404,
  InternalServerError = 500,
}

function handleResponse(status: HttpStatus): void {
  if (status === HttpStatus.Ok || status === HttpStatus.Created) {
    console.log("Success");
  } else if (status >= 400) {
    console.log("Error");
  }
}

const enums

const enum members are inlined at every usage point during compilation. The enum itself disappears from the output, producing smaller and faster code:

const enum Alignment {
  Left   = "LEFT",
  Center = "CENTER",
  Right  = "RIGHT",
}

const textAlign = Alignment.Center;
// Compiles to: const textAlign = "CENTER";
// No enum object exists at runtime

The tradeoff: const enum values cannot be iterated at runtime because the enum object does not exist.

Enums vs union literals

For most use cases, a union of string literals is a better choice than an enum:

// Enum approach
enum Status { Active = "active", Inactive = "inactive", Pending = "pending" }

// Union literal approach — simpler, no compile output, JSON-compatible
type Status = "active" | "inactive" | "pending";

Use enums when you need:

  • The enum object at runtime (e.g., to iterate all values)
  • Interoperability with code generators (Prisma, OpenAPI tools, gRPC)
  • Bitfield flags (const enum Permissions { Read = 1, Write = 2, Execute = 4 })

Use union literals for everything else.

Practice

  1. Create a readonly array of supported themes ("light" | "dark" | "system"). Write a function that accepts a theme and returns a CSS class name. Verify that passing an unsupported string is a compile error.
  2. Define a named tuple ApiResult with fields [status: number, data: unknown, error?: string]. Write a function that returns this tuple and destructure the result at the call site.
  3. Convert an enum you find in an existing codebase (or write a new one) to a union literal type. Verify that all existing usages still compile correctly.

Prefer union literals over enums in most cases
Union types like `type Direction = 'north' | 'south' | 'east' | 'west'` are simpler, compile away completely, work naturally with JSON, and do not require importing an enum at every use site. Reserve enums for cases where you need the enum object at runtime (e.g., iterating its values) or when working with code generators that produce enums.
Numeric enums have a reverse mapping pitfall
Regular numeric enums generate a reverse mapping in the compiled JavaScript: `Direction[0]` returns the key name. This bloats the output and can cause confusion. Always prefer string enums or const enums when working with TypeScript exclusively.
as const for literal tuple inference
When you define a tuple literal without an explicit type annotation, TypeScript widens the type to `(string | number)[]`. Add `as const` to preserve the exact literal types: `const point = [0, 0] as const` is inferred as `readonly [0, 0]` — a tuple of two literal zeros.
// Typed arrays — two equivalent syntaxes
const scores: number[] = [95, 87, 72];
const names: Array<string> = ["Alice", "Bob", "Carol"];

// Readonly array — elements cannot be added, removed, or replaced
const config: readonly string[] = ["dark", "compact", "rtl"];
config.push("new");     // Error: Property 'push' does not exist on type 'readonly string[]'
config[0] = "light";   // Error: Index signature in type 'readonly string[]' only permits reading

// ReadonlyArray<T> is equivalent to readonly T[]
const ids: ReadonlyArray<number> = [1, 2, 3];