| |

TypeScript 5 🔷 Arrays, Tuples, and Enums

Once you have the primitives, the next step is collections and named values. Arrays hold many values of the same type. Tuples hold a fixed number of values of different types, position by position. Enums give names to a set of related constants. These three types appear constantly — arrays in every list, tuples in function returns and destructuring, enums in status codes and configuration. Each has a different shape and a different set of trade-offs.

Key point: Arrays are homogeneous and variable-length. Tuples are heterogeneous and fixed-length. Enums are named constants with a runtime footprint. TypeScript gives you all three, but for constants the modern preference is often a union of literals instead of an enum. Learn all three; use the right one for the job.


Arrays

An array type is written as T[] or Array<T>. Both mean the same thing.

let names: string[] = ['Alice', 'Bob'];
let ages: number[] = [30, 25];
let flags: boolean[] = [true, false];

let sameAsAbove: Array<string> = ['Alice', 'Bob'];

string[] is preferred for simple types. Array<T> is used when the element type is complex — Array<string | number> reads better than (string | number)[].

What arrays support:

  • All array methods — .map, .filter, .reduce, .find, .forEach
  • .length — a number
  • Index access — names[0] gives string
  • for...of, for...in, spread — all work

Element access is always the same type:

const names: string[] = ['Alice', 'Bob'];
const first = names[0];          // string
const maybe = names[10];         // string — but undefined at runtime

TypeScript types names[10] as string even though it’s undefined at runtime. To catch this, enable noUncheckedIndexedAccess in tsconfig.json — then every array access is T | undefined.

Empty arrays need annotation:

let a = [];                       // any[]
let b: string[] = [];             // string[]
let c = [] as string[];           // string[]

[] alone is any[] — dangerous. Annotate or use as.

Readonly arrays:

const nums: readonly number[] = [1, 2, 3];
nums.push(4);                     // ❌ no push
nums[0] = 10;                     // ❌ no mutation

readonly T[] (or ReadonlyArray<T>) prevents mutation. Useful for function parameters when you want to promise you won’t modify the caller’s array.

Why two syntaxes (T[] and Array<T>): They’re identical. T[] is shorter; Array<T> is more consistent with generics and clearer when T is a union. Pick the one that reads better — most codebases use T[] for simple types.


Inference and annotation for arrays

TypeScript infers array types from initializers.

const nums = [1, 2, 3];           // number[]
const mixed = [1, 'a', true];     // (string | number | boolean)[]
const nested = [[1, 2], [3, 4]];  // number[][]

Heterogeneous arrays widen to a union of element types. [1, 'a'] is (string | number)[] — every element is either a string or a number.

Empty array with annotation:

const users: User[] = [];
users.push({ id: 1, name: 'Alice' });  // ✅
users.push({ id: 2 });                 // ❌ missing name

Array.from and Array.of:

const from = Array.from('abc');         // string[]
const of = Array.of(1, 2, 3);           // number[]
const mapped = Array.from([1, 2, 3], n => n * 2);  // number[]

Spread and concat:

const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b];           // number[]
const concatenated = a.concat(b);        // number[]

Both produce number[]. Spread is preferred in modern code.

Map and filter preserve element type (mostly):

const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2);    // number[]
const strings = nums.map(n => `${n}`);   // string[]
const even = nums.filter(n => n % 2 === 0);  // number[]

.map and .filter are generic — they infer the result element type from the callback’s return type.

Why [1, 'a'] widens to a union: TypeScript doesn’t know your intent. It could be an array of strings, an array of numbers, or a mixed array. Widening to the union (string | number)[] is the safe choice — every element may be either. If you want a specific type, annotate or use a tuple.


Multidimensional arrays

An array of arrays is written T[][].

const grid: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

const first = grid[0];        // number[]
const cell = grid[0][0];      // number

Jagged arrays — rows of different lengths — are still T[][].

const jagged: number[][] = [
  [1],
  [2, 3],
  [4, 5, 6]
];

TypeScript doesn’t enforce rectangular shape. If you need fixed dimensions, use a tuple.

Three dimensions:

const cube: number[][][] = [
  [[1, 2], [3, 4]],
  [[5, 6], [7, 8]]
];

Each [] adds a dimension. Read left to right: number → array of number → array of array of number → …

Why no rectangular enforcement: JavaScript arrays don’t have a fixed shape. TypeScript follows the language. If you need a matrix with a specific shape, a tuple is the type-level tool — not an array.


Tuples

A tuple is a fixed-length array where each position has a specific type.

let point: [number, number] = [3, 4];
let pair: [string, number] = ['age', 30];
let entry: [string, string, number] = ['Alice', 'Johnson', 30];

Position matters. ['age', 30] is a [string, number]; [30, 'age'] is not.

Access:

const x = point[0];              // number
const y = point[1];              // number
point[2];                        // ❌ tuple has no index 2

TypeScript knows the exact type at each index. Accessing beyond the length is a compile error.

Destructuring:

const [x, y] = point;
const [key, value] = pair;

The destructured variables get the tuple’s element types.

Fixed length:

let p: [number, number] = [1, 2];
p = [1, 2, 3];                   // ❌ too many
p = [1];                         // ❌ too few

TypeScript enforces exactly two elements.

Tuples are still arrays:

point.map(n => n * 2);           // number[]
point.length;                    // 2
[...point, 5];                   // number[]

Tuples inherit array methods. Spreading them into a normal array gives a regular array type.

Why tuples exist: Some data is positional — coordinates, key-value pairs, return values with multiple parts. Objects can model these too ({ x: number, y: number }), but tuples are more compact when names are obvious from context. They’re also how TypeScript types the result of things like Object.entries (which returns [string, T][]).


Optional and rest elements in tuples

Tuples can have optional and rest elements.

Optional elements — ?:

type Pair = [string, number?];
const a: Pair = ['age'];
const b: Pair = ['age', 30];

Optional elements must come after required ones. [string?, number] is invalid — required elements can’t follow optional ones.

Rest elements — ...:

type Strings = [string, ...string[]];
const s1: Strings = ['a'];
const s2: Strings = ['a', 'b', 'c'];

type Mixed = [string, ...number[]];
const m: Mixed = ['sum', 1, 2, 3];

A rest element at the end allows any number of additional elements of a given type. It’s the tuple equivalent of a rest parameter.

Labeled tuples:

type Point = [x: number, y: number];
type Entry = [key: string, value: number];

const p: Point = [3, 4];          // labels are documentation only

Labels don’t affect behavior — they’re editor hints. They make complex tuples readable.

Readonly tuples:

const point: readonly [number, number] = [3, 4];
point[0] = 5;                    // ❌ no mutation

readonly [number, number] prevents mutation. Common for constants like as const arrays.

as const — the const tuple trick:

const point = [3, 4] as const;    // readonly [3, 4]
const arr = ['a', 'b'] as const;  // readonly ['a', 'b']

as const freezes the array type — each element gets its literal type, and the tuple becomes readonly. Extremely useful for configuration and constants.

Why rest elements in tuples: They let you model “exactly one string followed by any number of numbers” — a shape that plain arrays can’t express. Combined with labeled tuples, they make function signatures and complex return types self-documenting.


When to use tuples vs objects

Both can model the same data.

// Tuple
type PointTuple = [number, number];
const p1: PointTuple = [3, 4];

// Object
type PointObj = { x: number; y: number };
const p2: PointObj = { x: 3, y: 4 };

Choose tuples when:

  • The data is positional and the positions are obvious from context
  • You’re returning multiple values from a function
  • You’re modeling a sequence where position carries meaning — [key, value], [latitude, longitude]

Choose objects when:

  • The data has named fields that readers need
  • The data may evolve (adding a field to a tuple breaks every call site)
  • The data has optional or default values
  • The data is passed across module boundaries

Real-world guidance: In 90% of cases, an object is better. It’s self-documenting, it’s extensible, and it’s friendlier to refactoring. Tuples shine in narrow cases: return values, destructuring patterns, and anywhere the position itself is the meaning.

Why objects usually win: A tuple’s shape is invisible at the call site. f([1, 2]) doesn’t tell you what 1 and 2 mean. f({ x: 1, y: 2 }) does. For anything a reader needs to understand, use an object. Tuples are for when the meaning is obvious or documented elsewhere.


Enums

An enum defines a set of named constants.

enum Status {
  Loading,
  Success,
  Error
}

let s: Status = Status.Loading;

By default, the values are numeric — 0, 1, 2. Enums create a runtime object with both directions:

Status.Loading;    // 0
Status[0];         // 'Loading'

That reverse mapping is unique to numeric enums.

String enums:

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT'
}

String enums don’t have reverse mapping — only Direction.Up gives 'UP'. String enums are generally safer because their values don’t depend on order and they serialize predictably.

Const enums:

const enum Color {
  Red = 'red',
  Green = 'green'
}

const c = Color.Red;              // inlines to 'red'

const enum is inlined at compile time — no runtime object is emitted. Faster at runtime, but breaks with isolatedModules and is discouraged in modern TypeScript.

Heterogeneous enums — mixing string and number — are allowed but confusing. Avoid.

Enums in modern TypeScript:

Enums are one of the most debated features. Problems:

  • They emit runtime code — not erased like most TypeScript
  • Numeric enums allow any number to be assigned (let s: Status = 99)
  • They don’t work well with isolatedModules or some bundlers
  • const enum has its own issues

The modern alternative — union of literals:

type Status = 'loading' | 'success' | 'error';
const status: Status = 'loading';

Or with constants:

const Status = {
  Loading: 'loading',
  Success: 'success',
  Error: 'error'
} as const;

type Status = typeof Status[keyof typeof Status];
// 'loading' | 'success' | 'error'

The as const object gives you named values and a union type — no runtime enum object needed.

When to still use enums:

  • Existing codebases that already use them
  • APIs that return numeric codes (numeric enums match)
  • Scenarios where you want the runtime object without extra boilerplate

When to prefer unions/as const:

  • New projects, especially with isolatedModules
  • When you want types that erase at runtime
  • When you want strict enforcement of allowed values

Why enums are debated: They’re one of the few TypeScript features that emit runtime code and change JavaScript’s behavior. A enum becomes an object in the output — that’s a side effect other TS features don’t have. The as const pattern achieves the same result with plain JavaScript objects, so most modern guidance prefers it.


Numeric enum gotchas

Numeric enums allow any number, not just the ones you defined.

enum Status { Loading, Success, Error }

let s: Status = 99;              // ✅ compiles
let s2: Status = Status.Loading; // ✅

TypeScript accepts any number for a numeric enum — the type is effectively number. That defeats much of the purpose.

String enums are stricter:

enum Direction { Up = 'UP', Down = 'DOWN' }

let d: Direction = 'UP';         // ❌ string literal not assignable
let d2: Direction = Direction.Up; // ✅

String enums only accept values from the enum. This is why string enums are preferred when you do use enums.

Reverse mapping surprise:

enum E { A, B }
E[0];                            // 'A'
Object.keys(E);                  // ['0', '1', 'A', 'B']

Numeric enums have bidirectional entries. Iterating over an enum with Object.keys gives both names and numeric values. Use Object.values with a filter or iterate with Object.entries carefully.

Why numeric enums are loose: They existed before strict type checking was the norm. TypeScript keeps them loose for backward compatibility. In new code, use string enums or as const objects — both are stricter.


A full example

A small program that uses arrays, tuples, and enums together.

// Array of users
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Carol' }
];

// Tuple: [key, value] entries
const entries: [string, number][] = [
  ['alice', 30],
  ['bob', 25],
  ['carol', 35]
];

// Tuple for a point
type Point = [x: number, y: number];
const origin: Point = [0, 0];
const target: Point = [10, 20];

// Enum for status
enum Status {
  Idle = 'IDLE',
  Loading = 'LOADING',
  Ready = 'READY',
  Error = 'ERROR'
}

let current: Status = Status.Idle;

function setStatus(next: Status): void {
  current = next;
  console.log('Status:', next);
}

setStatus(Status.Loading);
setStatus(Status.Ready);

// Union-of-literals alternative
type Direction = 'north' | 'south' | 'east' | 'west';
const facing: Direction = 'north';

console.log(users.length, entries[0], origin, target, facing);

Each piece does its job: users is a homogeneous array, entries is an array of tuples, point is a labeled tuple, Status is a string enum, Direction is a union of literals.

Why show both enums and unions: Modern TypeScript often prefers unions over enums. Seeing both side by side shows they solve the same problem. Choose based on whether you need the runtime object (enum) or the erasure (union).


Complete Example Session

# ============================================
# PART 1: BASIC ARRAYS
# ============================================

cat > arrays.ts << 'EOF'
const names: string[] = ['Alice', 'Bob'];
const ages: number[] = [30, 25];
const flags: boolean[] = [true, false];

const doubled = ages.map(n => n * 2);
const even = ages.filter(n => n % 2 === 0);

console.log(names[0], doubled, even);
EOF

npx tsc --noEmit arrays.ts
# (no errors)

# ============================================
# PART 2: ARRAY ERRORS
# ============================================

cat > array-errors.ts << 'EOF'
const names: string[] = ['Alice'];
names.push(42);                    // ❌ number not string
const first: number = names[0];    // ❌ string not number
EOF

npx tsc --noEmit array-errors.ts
# [ array-errors.ts:2:12 - Argument of type 'number' is not assignable to parameter of type 'string'. ]
# [ array-errors.ts:3:7  - Type 'string' is not assignable to type 'number'. ]

rm array-errors.ts

# ============================================
# PART 3: TUPLES
# ============================================

cat > tuples.ts << 'EOF'
type Point = [x: number, y: number];
const origin: Point = [0, 0];
const [x, y] = origin;

const entries: [string, number][] = [
  ['alice', 30],
  ['bob', 25]
];

for (const [key, value] of entries) {
  console.log(key, value);
}

// Optional and rest
type Pair = [string, number?];
const a: Pair = ['age'];
const b: Pair = ['age', 30];

type Mixed = [string, ...number[]];
const m: Mixed = ['sum', 1, 2, 3];

console.log(x, y, a, b, m);
EOF

npx tsc --noEmit tuples.ts
# (no errors)

# ============================================
# PART 4: TUPLE ERRORS
# ============================================

cat > tuple-errors.ts << 'EOF'
const p: [number, number] = [1, 2, 3];    // ❌ too many
const q: [number, number] = [1];          // ❌ too few
const r: [number, number] = ['a', 2];     // ❌ wrong type
EOF

npx tsc --noEmit tuple-errors.ts
# [ tuple-errors.ts:1:29 - Source has 3 element(s) but target allows only 2. ]
# [ tuple-errors.ts:2:29 - Source has 1 element(s) but target requires 2. ]
# [ tuple-errors.ts:3:29 - Type 'string' is not assignable to type 'number'. ]

rm tuple-errors.ts

# ============================================
# PART 5: ENUMS
# ============================================

cat > enums.ts << 'EOF'
enum Status {
  Loading,
  Success,
  Error
}

let s: Status = Status.Loading;
console.log(Status[s]);

enum Direction {
  Up = 'UP',
  Down = 'DOWN'
}

let d: Direction = Direction.Up;
console.log(d);

// Modern alternative
const Status2 = {
  Loading: 'loading',
  Success: 'success',
  Error: 'error'
} as const;
type Status2 = typeof Status2[keyof typeof Status2];

const t: Status2 = 'loading';
console.log(t);
EOF

npx tsc --noEmit enums.ts
# (no errors)

# ============================================
# PART 6: COMPILE AND RUN
# ============================================

npx tsc arrays.ts tuples.ts enums.ts
node arrays.js
# [ Alice [ 60, 50 ] [ 30 ] ]

node tuples.js
# [ alice 30 ]
# [ bob 25 ]
# [ 0 0 [ 'age' ] [ 'age', 30 ] [ 'sum', 1, 2, 3 ] ]

node enums.js
# [ Loading ]
# [ UP ]
# [ loading ]

Quick Reference

Array Syntax

FormMeaning
string[]Array of strings
Array<string>Same as string[]
(string | number)[]Array of union
readonly string[]Immutable array
number[][]Array of arrays
User[]Array of User

Array Inference

InitializerType
[1, 2, 3]number[]
['a', 'b']string[]
[1, 'a'](string | number)[]
[]any[] (annotate!)
[[1], [2]]number[][]

Tuple Syntax

FormMeaning
[string, number]Two elements, specific types
[string, number?]Second optional
[string, ...number[]]First string, then rest numbers
[x: number, y: number]Labeled
readonly [number, number]Immutable tuple

Tuple vs Array

AspectArrayTuple
LengthVariableFixed (or rest)
Element typesUniformPositional
Access beyond lengthT (unsafe)Compile error
Use caseListsPositional data

Tuple vs Object

AspectTupleObject
Readable at call site
Extensible
Compact
DestructuringPositionalBy name
Best forPositional dataNamed data

Enum Syntax

FormValues
enum E { A, B }0, 1
enum E { A = 1, B = 2 }1, 2
enum E { A = 'a' }'a'
const enum E { A }Inlined

Enums vs Union of Literals

AspectEnumUnion
Runtime code
Erased
Strict values⚠️ Numeric loose
Reverse mapping✅ (numeric)
Works with isolatedModules⚠️
Modern preferenceLegacy

Common Array Methods

MethodReturns
.map(fn)U[]
.filter(fn)T[]
.reduce(fn, init)U
.find(fn)T | undefined
.some(fn)boolean
.every(fn)boolean
.includes(x)boolean
.lengthnumber

as const Effects

BeforeAfter as const
string[]readonly ['a', 'b']
number[]readonly [3, 4]
{ a: 'x' }{ readonly a: 'x' }
Element typesLiterals

Best Practices

Do This:

// Annotate empty arrays
const users: User[] = [];                               // ✅

// Use readonly for immutable params
function process(items: readonly string[]): void { }    // ✅

// Use `T[]` for simple types
const names: string[] = [];                             // ✅

// Use Array<T> for unions
const mixed: Array<string | number> = [];               // ✅

// Use tuples for positional returns
function minMax(xs: number[]): [number, number] { }     // ✅

// Label tuples for readability
type Point = [x: number, y: number];                    // ✅

// Use `as const` for constant arrays
const COLORS = ['red', 'green'] as const;               // ✅

// Prefer string enums over numeric
enum Status { A = 'A', B = 'B' }                        // ✅

// Or use unions of literals
type Status = 'loading' | 'ready' | 'error';            // ✅

Don’t Do This:

// Don't leave arrays untyped
const items = [];                                       // ⚠️  any[]

// Don't use Array<T> for simple types
const names: Array<string> = [];                        // ⚠️  string[] is cleaner

// Don't use tuples for complex objects
type User = [string, number, boolean, string];          // ❌ use an interface

// Don't use numeric enums in new code
enum Status { A, B, C }                                 // ⚠️  any number assignable

// Don't use const enums with isolatedModules
const enum E { A }                                      // ⚠️  breaks isolatedModules

// Don't mix string and number in enums
enum Mixed { A = 1, B = 'b' }                           // ❌

// Don't forget `as const` for literal arrays
const dirs = ['north', 'south'];  // string[]              // ⚠️  not literals

// Don't access arrays beyond bounds
const xs = [1, 2, 3];
const y = xs[10];  // typed as number                   // ⚠️  enable noUncheckedIndexedAccess

Common Pitfalls

PitfallProblemSolution
[] without annotationany[]Annotate
(string | number)[] ambiguityUnexpected unionsAnnotate or use tuple
Tuple vs array mismatchLength/type errorsUse correct form
Confusing optional and restWrong shape? for optional, ... for rest
Numeric enum loosenessAny number assignableUse string enums
const enum with isolatedModulesCompile errorUse regular enum or as const
Forgetting as constWidened literalsAdd as const
Array access out of boundsSilent undefinedEnable noUncheckedIndexedAccess
Mutating readonly arraysCompile errorCopy with spread first
Reverse-mapping surpriseNumeric enum keysIterate carefully

Real-World Examples

1. Array of strings

const names: string[] = ['Alice', 'Bob'];

2. Array of objects

interface User { id: number; name: string; }
const users: User[] = [{ id: 1, name: 'Alice' }];

3. Empty array with type

const results: number[] = [];

4. Readonly array parameter

function sum(nums: readonly number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

5. Multidimensional array

const grid: number[][] = [[1, 2], [3, 4]];

6. Tuple for coordinates

type Point = [x: number, y: number];
const p: Point = [3, 4];

7. Tuple for key-value

const entry: [string, number] = ['age', 30];
const [key, value] = entry;

8. Array of tuples

const pairs: [string, number][] = [
  ['alice', 30],
  ['bob', 25]
];

9. Function returning tuple

function minMax(xs: number[]): [number, number] {
  return [Math.min(...xs), Math.max(...xs)];
}

10. Optional tuple element

type Pair = [string, number?];
const a: Pair = ['age'];
const b: Pair = ['age', 30];

11. Rest element in tuple

type Args = [string, ...number[]];
const a: Args = ['sum', 1, 2, 3];

12. as const array

const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];  // 'red' | 'green' | 'blue'

13. String enum

enum Direction {
  Up = 'UP',
  Down = 'DOWN'
}

14. Union of literals (enum alternative)

type Status = 'loading' | 'ready' | 'error';

15. as const object as enum

const Status = {
  Loading: 'loading',
  Ready: 'ready'
} as const;
type Status = typeof Status[keyof typeof Status];

16. Iterate a tuple

const point: [number, number] = [3, 4];
for (const n of point) console.log(n);

17. Object.entries typed

const obj = { a: 1, b: 2 };
const entries: [string, number][] = Object.entries(obj);

18. Filter with type guard

const mixed: (string | number)[] = [1, 'a', 2];
const nums = mixed.filter((x): x is number => typeof x === 'number');

19. Non-null after find

const user = users.find(u => u.id === 1);
if (user) { /* user is User */ }

20. Spread to combine arrays

const a = [1, 2];
const b = [3, 4];
const all = [...a, ...b];  // number[]

Visual: Array vs Tuple

┌──────────────────────────────────────────────┐
│  Array — variable length, uniform type       │
│                                              │
│  let xs: number[] = [1, 2, 3, 4, 5];         │
│                                              │
│  ✅ xs.push(6)                               │
│  ✅ xs.length can change                     │
│  ✅ every element is number                  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Tuple — fixed length, positional types      │
│                                              │
│  let p: [number, string] = [1, 'a'];         │
│                                              │
│  ❌ p.push(2)  — but not from a strict TS    │
│  ❌ p = [1]    — too few                     │
│  ✅ p[0] is number                           │
│  ✅ p[1] is string                           │
│                                              │
└──────────────────────────────────────────────┘

Visual: Tuple Shapes

┌──────────────────────────────────────────────┐
│  [string, number]                            │
│                                              │
│  ['a', 1]              ✅                    │
│  ['a', 'b']            ❌ second is string   │
│  [1, 'a']              ❌ first is number    │
│  ['a', 1, 2]           ❌ too many           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  [string, number?]                           │
│                                              │
│  ['a']                 ✅                    │
│  ['a', 1]              ✅                    │
│  ['a', 1, 2]           ❌ too many           │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  [string, ...number[]]                       │
│                                              │
│  ['a']                 ✅                    │
│  ['a', 1]              ✅                    │
│  ['a', 1, 2, 3]        ✅                    │
│  [1, 'a']              ❌ first must be string│
│                                              │
└──────────────────────────────────────────────┘

Visual: Enum vs as const

┌──────────────────────────────────────────────┐
│  Enum                                        │
│                                              │
│  enum Status { Loading = 'loading' }         │
│                                              │
│  Compiled JS:                                │
│  var Status;                                 │
│  (function (Status) {                        │
│    Status["Loading"] = "loading";            │
│  })(Status || (Status = {}));                │
│                                              │
│  → runtime object emitted                    │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  `as const` object                           │
│                                              │
│  const Status = {                            │
│    Loading: 'loading'                        │
│  } as const;                                 │
│                                              │
│  Compiled JS:                                │
│  const Status = { Loading: 'loading' };      │
│                                              │
│  → plain object, no boilerplate              │
│                                              │
└──────────────────────────────────────────────┘

Visual: as const Array

┌──────────────────────────────────────────────┐
│  Without `as const`                          │
│                                              │
│  const colors = ['red', 'green'];            │
│  // type: string[]                           │
│                                              │
│  → every element is `string`                 │
│  → mutable                                   │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  With `as const`                             │
│                                              │
│  const colors = ['red', 'green'] as const;   │
│  // type: readonly ['red', 'green']          │
│                                              │
│  → elements are literal types                │
│  → immutable                                 │
│  → type Color = typeof colors[number]        │
│    = 'red' | 'green'                         │
│                                              │
└──────────────────────────────────────────────┘

Visual: Arrays in Method Chains

┌──────────────────────────────────────────────┐
│  const nums = [1, 2, 3, 4, 5];               │
│                                              │
│  nums                                         │
│    .filter(n => n % 2 === 0)  → number[]     │
│    .map(n => n * 10)          → number[]     │
│    .reduce((a, b) => a + b)   → number       │
│                                              │
│  Result: 60                                  │
│                                              │
│  Each step is typed — inference flows        │
│  through the chain.                          │
│                                              │
└──────────────────────────────────────────────┘

Visual: When to Use Which

┌──────────────────────────────────────────────┐
│  Array                                       │
│                                              │
│  A list of items                             │
│  Variable length, same type                  │
│                                              │
│  users, names, prices                        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Tuple                                       │
│                                              │
│  Fixed positional data                       │
│  Different types at each position            │
│                                              │
│  [x, y]  [key, value]  [min, max]            │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Enum / union                                │
│                                              │
│  Named constants                             │
│  A closed set of values                      │
│                                              │
│  Status, Direction, Role                     │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptMeaning
T[] / Array<T>Array of T
readonly T[]Immutable array
T[][]Multidimensional array
[A, B]Tuple with two specific elements
[A, B?]Optional second element
[A, ...B[]]Rest element
readonly [A, B]Immutable tuple
as constFreeze to literals
enumNamed constants with runtime code
const enumInlined constants
String enumEnum with string values
Union of literalsEnum alternative without runtime
as const objectEnum alternative with named access

Key takeaways:

  • Arrays are homogeneous and variable-lengthstring[], number[], User[]
  • Annotate empty arrays[] alone is any[]
  • readonly T[] prevents mutation — useful for function parameters
  • Multidimensional arrays are T[][] — jagged shapes are allowed
  • Tuples are fixed-length, positional[number, string], [x: number, y: number]
  • Tuples support optional (?) and rest (...) elements
  • as const turns an array into a readonly tuple of literals — the go-to for constant arrays
  • Tuples are for positional data; objects for named data — objects win in most cases
  • Enums have runtime footprint — they emit a JavaScript object
  • String enums are stricter than numeric ones
  • Numeric enums accept any number — a well-known looseness
  • Union of literals and as const objects are the modern alternatives to enums
  • Use as const for constant arrays and typeof x[number] for the element union

Remember: Arrays are your lists — use them for collections of the same kind of thing. Tuples are for positional data — coordinates, key-value pairs, multiple return values. Enums name a set of related constants, but in new code, a union of literals or an as const object often does the same job without the runtime footprint. Pick based on whether the data is a list, a position, or a named set. That’s the whole decision.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!