TypeScript Array of Vectors: Tuples, Math and Typed Arrays

TypeScript has no Vector class. Model each vector as a tuple such as [x: number, y: number, z: number], store them in an array, and do vector math with typed functions.

A TypeScript array of vectors is an array whose elements are short arrays of numbers, such as 3D points [x, y, z], so the whole structure is a two-dimensional array. TypeScript has no built-in Vector type, so we give each vector a tuple type such as [number, number, number] and declare the outer array as Vec3[]. The compiler checks that every vector has three numbers, which a plain number[][] cannot do.

We use arrays of vectors for coordinates and directions, such as the points of a 3D model in a browser game or the GPS positions along a delivery route. Developers coming from C++ (std::vector) or Java (java.util.Vector) often search for “vector” when they want a growable list. In TypeScript, a growable list is a plain array, because number[] or string[] already grows and shrinks, so the vectors here are math vectors with a fixed number of components.

The following example declares a labeled tuple type Vec3 and runs the common operations on an array of vectors with the regular array methods, compiled with TypeScript 7.0.2 in strict mode and run on Node.js 22.

type Vec3 = [x: number, y: number, z: number];

// 1. Create an array of vectors
const points: Vec3[] = [
  [1, 2, 2],
  [3, 4, 0],
];

// 2. Add a vector and read values
points.push([0, 0, 1]);                                 // length = 3
const second = points[1];                               // second = [3, 4, 0]
const y = points[1][1];                                 // y = 4

// 3. Length (magnitude) of each vector
const lengths = points.map(([x, y, z]) => Math.hypot(x, y, z));   // lengths = [3, 5, 1]

// 4. Scale each vector; return a tuple, not number[]
const scaled = points.map(([x, y, z]): Vec3 => [x * 2, y * 2, z * 2]);
// scaled = [[2, 4, 4], [6, 8, 0], [0, 0, 2]]

// 5. Add all vectors together
const sum = points.reduce<Vec3>(
  ([ax, ay, az], [bx, by, bz]) => [ax + bx, ay + by, az + bz],
  [0, 0, 0],
);                                                      // sum = [4, 6, 3]

// 6. Create n zero vectors, each a separate array
const zeros = Array.from({ length: 2 }, (): Vec3 => [0, 0, 0]);   // zeros = [[0, 0, 0], [0, 0, 0]]

Notice that steps 4 and 6 give the callback the return type Vec3, so the results stay tuples instead of number[][].

Next, we compare the types that can represent a vector and the tuple pitfalls that the compiler does or does not catch. After that, we write vector math such as length and dot product, and we switch to typed arrays for large data sets.

1. Choosing a Type for a Vector

The right type depends on whether the number of components is fixed and whether the code reads them by position or by name. Tuples match the usual math notation and work with destructuring, so most examples use the tuple type.

TypeExampleFixed size checkedBest for
Tuple[number, number, number]Yes2D and 3D points, colors, small math vectors
Labeled tuple[x: number, y: number, z: number]YesSame as a tuple, with names in editor hints
Interface{ x: number; y: number; z: number }Yes, by property namesCode that reads v.x more than v[0], JSON data
Number arraynumber[]NoVectors whose length changes, such as feature lists
Typed arrayFloat64ArrayLength fixed at creationThousands of vectors, graphics, numeric code

An array of vectors is the vector type followed by [], or one large typed array.

  • A Vec3[] array holds tuples.
  • A Vector3[] array holds objects.
  • A number[][] array holds vectors of different lengths.
  • One Float64Array holds the components of many vectors, as we will see in section 6.

2. Declaring a Vector Type

A tuple type is an array type with a fixed length and a known type at each position. Labeled tuple elements, added in TypeScript 4.0, give the positions names. The labels appear in editor hints and error messages but do not change the type, so a Point3 value is assignable to a Vec3 and back.

// 1. Tuple: exactly three numbers
type Vec3 = [number, number, number];

// 2. Labeled tuple: same type, names shown in the editor
type Point3 = [x: number, y: number, z: number];

// 3. Read-only tuple: no push(), no assignment
type FixedVec3 = readonly [x: number, y: number, z: number];

// 4. Object with named fields
interface Vector3 {
  x: number;
  y: number;
  z: number;
}

// 5. Any number of components
type VecN = number[];

The compiler checks the tuple length when a value is assigned and when an index is read. One rule confuses many developers. The map() method on a tuple returns number[], not a tuple, because the library type of map() does not know that the callback keeps the length. The same file also shows how a readonly tuple blocks push() and index assignment.

type Vec3 = [number, number, number];
type FixedVec3 = readonly [x: number, y: number, z: number];

const a: Vec3 = [1, 2];
const b: Vec3 = [1, 2, 2];
const doubled: Vec3 = b.map((n) => n * 2);
const w = b[3];

const c: FixedVec3 = [3, 4, 0];
c.push(1);
c[0] = 5;
src/err.ts(4,7): error TS2322: Type '[number, number]' is not assignable to type 'Vec3'.
  Source has 2 element(s) but target requires 3.
src/err.ts(6,7): error TS2322: Type 'number[]' is not assignable to type 'Vec3'.
  Target requires 3 element(s) but source may have fewer.
src/err.ts(7,13): error TS2493: Tuple type 'Vec3' of length '3' has no element at index '3'.
src/err.ts(10,3): error TS2339: Property 'push' does not exist on type 'FixedVec3'.
src/err.ts(11,3): error TS2540: Cannot assign to '0' because it is a read-only property.

To scale a tuple and keep its type, we destructure it and build a new tuple with an explicit return type, as step 4 of the intro example does with ([x, y, z]): Vec3 => [x * 2, y * 2, z * 2].

A mutable tuple has one gap. The compiler allows push() on it, so the value can grow past three numbers at runtime while the type still says three.

const v: Vec3 = [1, 2, 2];
v.push(4);                                              // compiles
const size = v.length;                                  // size = 4 at runtime

When vectors should never change after creation, the readonly tuple type blocks push() at compile time. For example, a map app saves the corners of a delivery zone, and no code should move a corner after the zone is saved. With a readonly tuple, we create new vectors instead of changing existing ones, which is also what the math functions in section 5 do.

3. Creating an Array of Vectors

An array of vectors is declared like any other array of a named type. Options 1 to 3 differ only in the vector type, whereas options 4 and 5 show the one creation pattern that causes real bugs.

// 1. Array of tuples
const points: Vec3[] = [[1, 2, 2], [3, 4, 0]];

// 2. Array of objects
const objs: Vector3[] = [{ x: 1, y: 2, z: 2 }, { x: 3, y: 4, z: 0 }];

// 3. Vectors of different lengths: number[][]
const rows: number[][] = [[1, 2], [3, 4, 5], [6]];

// 4. n zero vectors, each a separate array
const zeros = Array.from({ length: 3 }, (): Vec3 => [0, 0, 0]);
zeros[0][0] = 9;                                        // zeros = [[9, 0, 0], [0, 0, 0], [0, 0, 0]]

// 5. Wrong: fill() puts the same array in every slot
const shared = new Array<Vec3>(3).fill([0, 0, 0]);
shared[0][0] = 9;                                       // shared = [[9, 0, 0], [9, 0, 0], [9, 0, 0]]

The fill() method evaluates its argument once and stores that one array in every slot. All three slots point to the same vector, so changing one vector changes all of them. The method Array.from() calls the function once per slot, so each slot gets its own array. The (): Vec3 return type in the callback keeps the result typed as Vec3[] instead of number[][].

Arrays of Vector3 objects (option 2) work like any TypeScript array of objects, so we search and sort them with the same callbacks.

4. Adding, Removing and Reading Vectors

Array methods such as push() and splice() work on an array of vectors the same way as on any array. Each vector is one element, so splice(1, 1) removes one whole vector, not one number. One index gives us a whole vector, and a second index gives one component of it.

const points: Vec3[] = [[1, 2, 2], [3, 4, 0]];

// 1. Add and remove
points.push([0, 0, 1]);                                 // [[1, 2, 2], [3, 4, 0], [0, 0, 1]]
points.splice(1, 0, [5, 5, 5]);                         // inserts at index 1
const removed = points.splice(1, 1);                    // removed = [[5, 5, 5]]
const last = points.pop();                              // last = [0, 0, 1]

// 2. Read a vector and one component
const first = points[0];                                // first = [1, 2, 2]
const y = points[1][1];                                 // y = 4
const [x0, y0, z0] = points[0];                         // x0 = 1, y0 = 2, z0 = 2

// 3. Loop with destructuring
for (const [x, y, z] of points) {
  console.log(x, y, z);                                 // 1 2 2, then 3 4 0
}

// 4. Find a vector by value: compare components, not references
const byRef = points.includes([1, 2, 2]);               // byRef = false
const byValue = points.some(([x, y, z]) => x === 1 && y === 2 && z === 2);   // byValue = true

Destructuring (const [x0, y0, z0] = points[0]) copies the three components into separate variables. It works the same way in a for…of loop and in callback parameters, which keeps vector code short.

Option 4 matters when we check whether a vector is already in the list, for example before adding a waypoint to the path in a drawing app. A vector is an array, and includes() compares arrays by reference. The literal [1, 2, 2] creates a new array, so it never equals an element of points, even with the same numbers. We compare the components with some() instead, or store a string key such as “1,2,2” in a Set when there are many lookups.

5. Vector Math With map() and reduce()

JavaScript has no vector operators, so a + b on two arrays does not add them component by component. We write small typed functions for the operations we need and apply them to the array with map() and reduce(). For example, a browser game moves each enemy toward the player by scaling the unit vector of the direction. Each function takes vectors as destructured parameters and returns a new tuple.

type Vec3 = [number, number, number];

// 1. Basic operations as small typed functions
const add = ([ax, ay, az]: Vec3, [bx, by, bz]: Vec3): Vec3 => [ax + bx, ay + by, az + bz];
const scale = ([x, y, z]: Vec3, k: number): Vec3 => [x * k, y * k, z * k];
const divide = ([x, y, z]: Vec3, k: number): Vec3 => [x / k, y / k, z / k];
const dot = ([ax, ay, az]: Vec3, [bx, by, bz]: Vec3): number => ax * bx + ay * by + az * bz;
const length = ([x, y, z]: Vec3): number => Math.hypot(x, y, z);

const points: Vec3[] = [[1, 2, 2], [3, 4, 0]];

// 2. Apply them to the whole array
const lengths = points.map(length);                     // lengths = [3, 5]
const doubled = points.map((v) => scale(v, 2));         // doubled = [[2, 4, 4], [6, 8, 0]]
const units = points.map((v) => divide(v, length(v)));  // units[1] = [0.6, 0.8, 0]
const total = points.reduce(add, [0, 0, 0]);            // total = [4, 6, 2]
const d = dot(points[0], points[1]);                    // d = 11
const longest = points.toSorted((a, b) => length(b) - length(a))[0];   // longest = [3, 4, 0]

Math.hypot() returns the square root of the sum of squares, which is the vector length; for [3, 4, 0] it is 5. A unit vector is the vector divided by its length, so units holds vectors of length 1 that point in the same directions. For [1, 2, 2], the result is [0.333…, 0.666…, 0.666…], because floating-point numbers cannot store one third without rounding.

In reduce(add, [0, 0, 0]), the zero vector is the starting value, and add is called once per vector with the running total. The ES2023 toSorted() method returns a sorted copy, so points keeps its order. We prefer dividing by the length over multiplying by 1 / length. In floating point, 3 * (1 / 5) gives 0.6000000000000001, while 3 / 5 gives 0.6.

For heavier math such as matrices or rotations, a library like gl-matrix is a better choice than hand-written functions.

6. Typed Arrays for Large Sets of Vectors

An array of tuples stores every vector as a separate JavaScript array, so thousands or millions of vectors mean many small objects for the garbage collector to track. A typed array such as Float64Array stores numbers of one fixed type in a single block of memory. We put the components of all vectors one after another and compute where each vector starts. For example, a 3D scanner app that loads a point cloud keeps all points in one Float64Array.

// 1. Three numbers per vector, stored in one flat array
const count = 2;
const data = new Float64Array(count * 3);               // 6 zeros
data.set([1, 2, 2], 0);                                 // vector 0
data.set([3, 4, 0], 3);                                 // vector 1

// 2. Read vector i
const i = 1;
const v = data.subarray(i * 3, i * 3 + 3);              // v = Float64Array [3, 4, 0]
const len = Math.hypot(...v);                           // len = 5

// 3. Float32Array uses less memory but rounds values
const f32 = Float32Array.of(0.1)[0];                    // f32 = 0.10000000149011612

Vector i starts at index i * 3. The subarray() method returns a view on the same memory without copying, so writing to v changes data. A typed array has a fixed length, so there is no push(); we allocate the size we need up front.

Two typed array types fit vector data.

  • Float64Array stores the same 64-bit numbers as regular JavaScript numbers.
  • Float32Array uses half the memory and is the format WebGL code commonly uses for vertex data, but it rounds values to 32-bit precision, as the last line shows.

We use typed arrays when the data is large or goes to a graphics or numeric API, and tuples everywhere else, because tuples are easier to read and work with all array methods.

7. Trying the Vector Code Locally

All vector snippets are in the typescript-array-of-vectors folder of the examples repository, split by section. The project uses TypeScript 7.0.2 with the ES2024 library, which toSorted() needs, and it requires Node.js 22 or a later release.

npm install
npm start

The output lists each section with the same numbers as the snippet comments, including the shared-array result from the fill() example.

8. Conclusion

An array of vectors in TypeScript is an array of tuples, objects or number arrays, because the language has no Vector class. A tuple type such as [x: number, y: number, z: number] gives fixed-size, named components, and a readonly tuple also blocks push().

We create vectors with Array.from() instead of fill(), and we compare them by components rather than with includes(). For vector math, small typed functions with map() and reduce() are enough, and we switch to a Float64Array when the number of vectors gets large.

9. References

The TypeScript Handbook describes tuple types, and MDN documents the runtime methods and typed arrays used in the examples.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.