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— anumber- Index access —
names[0]givesstring 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[]andArray<T>): They’re identical.T[]is shorter;Array<T>is more consistent with generics and clearer whenTis a union. Pick the one that reads better — most codebases useT[]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 likeObject.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 what1and2mean.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
isolatedModulesor some bundlers const enumhas 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
enumbecomes an object in the output — that’s a side effect other TS features don’t have. Theas constpattern 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 constobjects — 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
| Form | Meaning |
|---|---|
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
| Initializer | Type |
|---|---|
[1, 2, 3] | number[] |
['a', 'b'] | string[] |
[1, 'a'] | (string | number)[] |
[] | any[] (annotate!) |
[[1], [2]] | number[][] |
Tuple Syntax
| Form | Meaning |
|---|---|
[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
| Aspect | Array | Tuple |
|---|---|---|
| Length | Variable | Fixed (or rest) |
| Element types | Uniform | Positional |
| Access beyond length | T (unsafe) | Compile error |
| Use case | Lists | Positional data |
Tuple vs Object
| Aspect | Tuple | Object |
|---|---|---|
| Readable at call site | ❌ | ✅ |
| Extensible | ❌ | ✅ |
| Compact | ✅ | ❌ |
| Destructuring | Positional | By name |
| Best for | Positional data | Named data |
Enum Syntax
| Form | Values |
|---|---|
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
| Aspect | Enum | Union |
|---|---|---|
| Runtime code | ✅ | ❌ |
| Erased | ❌ | ✅ |
| Strict values | ⚠️ Numeric loose | ✅ |
| Reverse mapping | ✅ (numeric) | ❌ |
Works with isolatedModules | ⚠️ | ✅ |
| Modern preference | Legacy | ✅ |
Common Array Methods
| Method | Returns |
|---|---|
.map(fn) | U[] |
.filter(fn) | T[] |
.reduce(fn, init) | U |
.find(fn) | T | undefined |
.some(fn) | boolean |
.every(fn) | boolean |
.includes(x) | boolean |
.length | number |
as const Effects
| Before | After as const |
|---|---|
string[] | readonly ['a', 'b'] |
number[] | readonly [3, 4] |
{ a: 'x' } | { readonly a: 'x' } |
| Element types | Literals |
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
| Pitfall | Problem | Solution |
|---|---|---|
[] without annotation | any[] | Annotate |
(string | number)[] ambiguity | Unexpected unions | Annotate or use tuple |
| Tuple vs array mismatch | Length/type errors | Use correct form |
| Confusing optional and rest | Wrong shape | ? for optional, ... for rest |
| Numeric enum looseness | Any number assignable | Use string enums |
const enum with isolatedModules | Compile error | Use regular enum or as const |
Forgetting as const | Widened literals | Add as const |
| Array access out of bounds | Silent undefined | Enable noUncheckedIndexedAccess |
Mutating readonly arrays | Compile error | Copy with spread first |
| Reverse-mapping surprise | Numeric enum keys | Iterate 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
| Concept | Meaning |
|---|---|
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 const | Freeze to literals |
enum | Named constants with runtime code |
const enum | Inlined constants |
| String enum | Enum with string values |
| Union of literals | Enum alternative without runtime |
as const object | Enum alternative with named access |
Key takeaways:
- Arrays are homogeneous and variable-length —
string[],number[],User[] - Annotate empty arrays —
[]alone isany[] 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 constturns 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 constobjects are the modern alternatives to enums - Use
as constfor constant arrays andtypeof 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!