Fixed-Length Tuple via Generics
Constraining an argument to 'an array of exactly 4 numbers' is a common need (RGBA, 4x4 matrices, fixed-length feature vectors). TypeScript can express it with a recursive `TupleOf<T, N>` that builds up `[T, T, T, ...]` of the requested length. This snippet covers the recursive construction, a runtime helper that produces a value matching the type, and a length-comparison variant useful for richer constraints ("at least N", "between N and M").
563 views
15
type TupleOf<T, N extends number, R extends T[] = []> = R['length'] extends N ? R : TupleOf<T, N, [T, ...R]>;
type RGBA = TupleOf<number, 4>;
const pixel = [255, 128, 64, 200] as RGBA;
console.log(`pixel length ${pixel.length}`);
console.log(pixel.join(','));The recursion builds R (an array accumulator type) one element at a time and stops when R['length'] matches N. TypeScript treats tuple length as a numeric literal when the tuple is fully known, which is what makes this comparison possible at the type level. The default R extends T[] = [] lets callers write TupleOf<T, N> without supplying the accumulator. The technique is the building block for every length-aware utility (split, join, take, drop) in advanced TS libraries; understanding it unlocks the rest.
type TupleOfV2<T, N extends number, R extends T[] = []> = R['length'] extends N ? R : TupleOfV2<T, N, [T, ...R]>;
function makeTuple<T, N extends number>(value: T, length: N): TupleOfV2<T, N> {
return Array.from({ length }, () => value) as TupleOfV2<T, N>;
}
const zeros = makeTuple(0, 4);
const nines = makeTuple(9, 3);
console.log(`${zeros.length} zeros: ${zeros.join(',')}`);
console.log(`${nines.length} nines: ${nines.join(',')}`);Pairing the type with a constructor closes the loop: callers get a value whose runtime length matches the static length-tracked type. Array.from({ length }, () => value) is the cleanest way to build a fixed-length array of the same value, and the cast to TupleOfV2<T, N> is honest about the static-vs-runtime gap. This shape is what you want for default RGBA, identity-matrix initialisers, and zeroed feature vectors. Use a sparse Array(n) only if every slot will be reassigned before reading; otherwise Array.from keeps the values defined and avoids undefined holes.
type TupleOfV3<T, N extends number, R extends T[] = []> = R['length'] extends N ? R : TupleOfV3<T, N, [T, ...R]>;
type LengthOf<T extends ReadonlyArray<unknown>> = T['length'];
type Pair = TupleOfV3<string, 2>;
type Triple = TupleOfV3<string, 3>;
type IsLongerThanTwo<T extends ReadonlyArray<unknown>> = LengthOf<T> extends 0 | 1 | 2 ? false : true;
type A = IsLongerThanTwo<Pair>;
type B = IsLongerThanTwo<Triple>;
const aSentinel = false as A;
const bSentinel = true as B;
console.log(`pair longer than two ${aSentinel}`);
console.log(`triple longer than two ${bSentinel}`);LengthOf<T> = T['length'] lifts the runtime length into the type system, which is what makes 'at least N' constraints expressible. Combining it with a literal-type union (extends 0 | 1 | 2) lets the compiler decide a length-based predicate at type level. The same pattern can express at most N, exactly between A and B, or multiple of K (with a bit more recursion). These checks usually surface in libraries that need to reject too-short tuples (route param lists, vector dot-products) at compile time, instead of failing at runtime.
