Typing arrays
TypeScript has two kinds of type for an array. An array type says every element has the same type and the length varies: string[] and Array<string> are two notations for it, and they are the same type. A tuple type says the length is fixed and each position has its own type: [string, number] is one, and there is only one notation.
Inference always guesses the array type. That single fact causes most of the friction in this entry, and knowing it turns a confusing error into an expected one. The arrays page is the entry for what arrays do; this one is about describing them.
Create programs/typing-arrays.test.ts for this reference and keep it open. Start it with the assertion helpers used on this page:
import { assertEquals, assertStrictEquals, assertThrows } from "@std/assert";
Follow the page as you add and revise the runnable examples below that import.
one type, two spellings
Deno.test("one type, two spellings", () => {
const literal: string[] = ["fee", "fi"];
const generic: Array<string> = literal;
const backAgain: string[] = generic;
assertEquals(backAgain, ["fee", "fi"]);
});
Check programs/typing-arrays.test.ts
running 1 test from ./programs/typing-arrays.test.ts
one type, two spellings ... ok (298µs)
ok | 1 passed | 0 failed (1ms)
One type, two spellings, assignable in both directions with nothing to declare. T[] came first and Array<T> arrived with generics, which is the only reason there are two.
the brackets bind tightly
Deno.test("the brackets bind tightly", () => {
type MixedLiteral = (string | number)[];
type MixedGeneric = Array<string | number>;
type StringOrNumbers = string | number[];
const mixed: MixedLiteral = ["a", 1];
const alsoMixed: MixedGeneric = mixed;
const oneString: StringOrNumbers = "a";
const manyNumbers: StringOrNumbers = [1, 2];
// @ts-expect-error: Type '(string | number)[]' is not assignable to type 'StringOrNumbers'.
const notMixed: StringOrNumbers = ["a", 1];
assertEquals(alsoMixed, ["a", 1]);
assertStrictEquals(oneString, "a");
assertEquals(manyNumbers, [1, 2]);
assertEquals(notMixed as unknown[], ["a", 1]);
});
the brackets bind tightly ... ok (113µs)
string | number[] is a string or an array of numbers, which is almost certainly not what somebody typing quickly meant. (string | number)[] needs the parentheses, and Array<string | number> needs nothing. This is the strongest argument for the generic notation: as soon as the element type has more than one token, Array<T> reads as what it is and T[] needs punctuation to avoid meaning something else. It also lines up with Set<T> and Map<K, V>, and it cannot be confused with [T].
a tuple fixes the length and types each position
Deno.test("a tuple fixes the length and types each position", () => {
type Row = [name: string, age: number];
type OneString = [string];
const row: Row = ["Ada", 36];
const [name, age] = row;
assertStrictEquals(name, "Ada");
assertStrictEquals(age, 36);
assertStrictEquals(row.length, 2);
// @ts-expect-error: Type 'Row' is not assignable to type 'OneString'.
const single: OneString = row;
assertStrictEquals(single[0], "Ada");
});
a tuple fixes the length and types each position ... ok (30µs)
name is a string because it is in position 0, and age is a number because it is in position 1, the array pattern from the destructuring page reading positions off a type that knows them. The labels in [name: string, age: number] are documentation with no run-time or type-level effect, and they are worth writing, because a bare [string, number] tells a reader nothing about which is which. Note OneString: [string] is a tuple of exactly one string, not an array of strings, and that confusion is the other reason to prefer Array<T> over T[] for anyone new to the syntax.
inference gives you an array, never a tuple
needsPair demands exactly two numbers, and [1, 2] has exactly two. Hand the inferred variable over:
Deno.test("inference gives you an array, never a tuple", () => {
function needsPair(pair: readonly [number, number]): number {
return pair[0] + pair[1];
}
const inferred = [1, 2];
assertStrictEquals(needsPair(inferred), 3);
});
Check programs/typing-arrays.test.ts
TS2345 [ERROR]: Argument of type 'number[]' is not assignable to parameter of type 'readonly [number, number]'.
Target requires 2 element(s) but source may have fewer.
assertStrictEquals(needsPair(inferred), 3);
~~~~~~~~
at file:///programs/typing-arrays.test.ts:57:32
error: Type checking failed.
"Source may have fewer" is the whole story. [1, 2] was inferred as number[], a promise about the element type and no promise at all about the length, so it cannot satisfy a type that demands two. TypeScript cannot infer a tuple, and it is not being lazy: [1, 2] has at least six reasonable types, and the array type is the one that is right more often. So when you want a tuple you have to say so, and there are two ways: annotate the variable, which gives a mutable tuple, or use as const, which gives a readonly [1, 2] of literal types, more precise and unable to enter a mutable parameter, from the read-only page. The third option is to stop wanting a tuple, because if a function takes two numbers, two parameters are usually better than one array of two. Shield the refusal and pin both fixes:
Deno.test("inference gives you an array, never a tuple", () => {
function needsPair(pair: readonly [number, number]): number {
return pair[0] + pair[1];
}
const inferred = [1, 2];
// @ts-expect-error: Argument of type 'number[]' is not assignable to parameter of type 'readonly [number, number]'.
needsPair(inferred);
const annotated: [number, number] = [1, 2];
const asserted = [1, 2] as const;
assertStrictEquals(needsPair(annotated), 3);
assertStrictEquals(needsPair(asserted), 3);
assertEquals(inferred, [1, 2]);
});
inference gives you an array, never a tuple ... ok (40µs)
an empty literal accumulates the types you push
Deno.test("an empty literal accumulates the types you push", () => {
const evolving = [];
evolving.push(1);
const numbers: number[] = evolving;
evolving.push("two");
const mixed: (string | number)[] = evolving;
assertEquals(numbers as unknown[], [1, "two"]);
assertEquals(mixed, [1, "two"]);
});
an empty literal accumulates the types you push ... ok (45µs)
No error, no annotation, and no any left over. An empty array literal starts as an implicit any[] and TypeScript watches what you put into it: after the first push the variable is number[], and after the second it is (string | number)[]. This is called an evolving array type, and it is one of the friendlier things the inference does; note the small dishonesty the test pins along the way, since numbers was typed number[] at a moment that was true and the later push through the same reference put a string inside it. The rule is about when you look. Read an element before anything has settled the type, in a scratch file programs/observed.ts:
const observed = [];
console.log(observed[0]);
observed.push(1);
Check programs/observed.ts
TS7034 [ERROR]: Variable 'observed' implicitly has type 'any[]' in some locations where its type cannot be determined.
const observed = [];
~~~~~~~~
at file:///programs/observed.ts:1:7
TS7005 [ERROR]: Variable 'observed' implicitly has an 'any[]' type.
console.log(observed[0]);
~~~~~~~~
at file:///programs/observed.ts:2:13
Found 2 errors.
error: Type checking failed.
Two errors for one mistake: the declaration is flagged for being undetermined, and the use is flagged for observing an implicit any[]. Move the push above the read and both go away. Reading .length is not a use in this sense, because .length is a number whatever the elements are, so the rule is precise: the element type must be settled before you read it, and pushes settle it. The practical advice is therefore narrower than annotate-every-empty-array. Write const found = [] followed by pushes and you are fine; write it and pass it somewhere first, and annotate it. Delete the scratch file.
a non-empty literal fixes the element type at once
Deno.test("a non-empty literal fixes the element type at once", () => {
const numbers = [123];
// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'number'.
numbers.push("oak");
assertEquals(numbers as unknown[], [123, "oak"]);
});
a non-empty literal fixes the element type at once ... ok (22µs)
The evolving behaviour is only for a literal that starts empty. One element is enough to fix the type, so [123] is number[] for good and the string is refused, though the shielded push landed as every shielded write does. Which is the right default: a literal with contents is a statement about what belongs in it.
an interface can describe an array, until it cannot
Deno.test("an interface can describe an array, until it cannot", () => {
interface Names {
[index: number]: string;
}
interface NamesAndSurname {
[index: number]: string;
surname: string;
}
const names: Names = ["Huey", "Dewey", "Louie"];
const ducks: NamesAndSurname = { 0: "Huey", 1: "Dewey", surname: "Duck" };
// @ts-expect-error: Property 'surname' is missing in type 'string[]' but required in type 'NamesAndSurname'.
const notAnArray: NamesAndSurname = ["Huey", "Dewey"];
assertStrictEquals(names[0], "Huey");
assertStrictEquals(ducks[1], "Dewey");
assertStrictEquals(ducks.surname, "Duck");
assertStrictEquals(notAnArray.surname as unknown, undefined);
});
an interface can describe an array, until it cannot ... ok (28µs)
A number index signature is enough to describe an array, because that is what an array is, numeric keys holding values, as the object types page explains when it gets to why a number key is a string key. So Names accepts an array literal. Add a named property and the type stops describing arrays, because an array literal has no surname and there is no syntax to give it one in the same expression, so the type now describes an object that happens to have numeric keys, which ducks is.
ArrayLike is the shape without the class
The standard library's version of that interface is the one you will actually meet. Shield a write into it, and predict what Array.from reads out:
Deno.test("ArrayLike is the shape without the class", () => {
const arrayLike: ArrayLike<string> = { length: 2, 0: "a", 1: "b" };
// @ts-expect-error: Index signature in type 'ArrayLike<string>' only permits reading.
arrayLike[0] = "x";
assertEquals(Array.from(arrayLike), ["a", "b"]);
});
Check programs/typing-arrays.test.ts
running 8 tests from ./programs/typing-arrays.test.ts
...
ArrayLike is the shape without the class ... FAILED (8ms)
ERRORS
ArrayLike is the shape without the class => ./programs/typing-arrays.test.ts:118:6
error: AssertionError: Values are not equal.
[Diff] Actual / Expected
[
- "x",
+ "a",
"b",
]
FAILURES
ArrayLike is the shape without the class => ./programs/typing-arrays.test.ts:118:6
FAILED | 7 passed | 1 failed (10ms)
error: Test failed
The write went through, and Array.from reports "x". ArrayLike<T> is a read-only number index signature plus a length, and it is the parameter type of Array.from, which is why that function accepts a NodeList, an arguments object, or an object you wrote by hand; it is the type to reach for when you want to accept anything array-shaped without demanding a real array. The readonly on its index signature is why the assignment is refused, and the landing write shows the refusal is compile-time only, the same measurement the read-only page made of every collection type. Correct the prediction:
Deno.test("ArrayLike is the shape without the class", () => {
const arrayLike: ArrayLike<string> = { length: 2, 0: "a", 1: "b" };
// @ts-expect-error: Index signature in type 'ArrayLike<string>' only permits reading.
arrayLike[0] = "x";
assertEquals(Array.from(arrayLike), ["x", "b"]);
assertStrictEquals(arrayLike.length, 2);
});
ArrayLike is the shape without the class ... ok (30µs)
reading an element is typed as present, whether or not it is
names has one element, and names[1] is typed string. Predict its typeof:
Deno.test("reading an element is typed as present, whether or not it is", () => {
const names = ["Ada"];
const missing = names[1];
assertStrictEquals(typeof missing, "string");
});
Check programs/typing-arrays.test.ts
running 9 tests from ./programs/typing-arrays.test.ts
...
reading an element is typed as present, whether or not it is ... FAILED (8ms)
ERRORS
reading an element is typed as present, whether or not it is => ./programs/typing-arrays.test.ts:129:6
error: AssertionError: Values are not strictly equal.
[Diff] Actual / Expected
- undefined
+ string
FAILURES
reading an element is typed as present, whether or not it is => ./programs/typing-arrays.test.ts:129:6
FAILED | 8 passed | 1 failed (10ms)
error: Test failed
Typed string, holds undefined, with no cast and no any anywhere: an index signature promises more than the value can keep, the same hole the objects as dictionaries page measures for Record<string, T>. Correct the prediction, with .at as the method that is honest without any flag:
Deno.test("reading an element is typed as present, whether or not it is", () => {
const names = ["Ada"];
const missing = names[1];
assertThrows(
() => missing.toUpperCase(),
TypeError,
"Cannot read properties of undefined",
);
const checked = names.at(1);
assertStrictEquals(checked, undefined);
});
reading an element is typed as present, whether or not it is ... ok (281µs)
noUncheckedIndexedAccess closes the hole by typing every index read as string | undefined. It is off by default, in Deno as in tsc, and turning it on finds real bugs; once it is on, the narrowing is worth knowing, because the obvious check does not work. Probe it with a scratch config programs/unchecked.json holding {"compilerOptions": {"noUncheckedIndexedAccess": true}} and a scratch file programs/narrowing.ts:
const names: string[] = ["Ada"];
export const direct: string = names[0];
export function viaLength(): string {
if (names.length > 0) {
return names[0];
}
return "";
}
export function viaIn(): string {
if (0 in names) {
return names[0];
}
return "";
}
export function viaLocal(): string {
const first = names[0];
return first === undefined ? "" : first;
}
Run deno check --config programs/unchecked.json programs/narrowing.ts:
Check programs/narrowing.ts
TS2322 [ERROR]: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
export const direct: string = names[0];
~~~~~~
at file:///programs/narrowing.ts:3:14
TS2322 [ERROR]: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
return names[0];
~~~~~~
at file:///programs/narrowing.ts:7:5
Found 2 errors.
error: Type checking failed.
Two errors, and their placement is the finding. The direct read fails, as it should. viaLength fails too, because the checker does not connect a length to an index, so after if (names.length > 0) the element is still string | undefined. And viaIn and viaLocal pass clean: 0 in names narrows the read, and assigning to a local and testing the local narrows the local. Prefer the local, which reads as ordinary code and does not depend on the reader knowing that in has this effect on arrays. Delete the two scratch files.
take readonly array parameters
Deno.test("take readonly array parameters", () => {
function sum(numbers: readonly number[]): number {
return numbers.reduce((total, next) => total + next, 0);
}
const mutable = [1, 2, 3];
const frozen = [1, 2, 3] as const;
assertStrictEquals(sum(mutable), 6);
assertStrictEquals(sum(frozen), 6);
});
take readonly array parameters ... ok (29µs)
A readonly parameter accepts a mutable array and an as const one, so it costs nothing and accepts strictly more callers; the reverse is not true, and the read-only page is the entry for why. One more thing this page owes you: the notation question. Deno's linter has no rule about array notation, so T[] against Array<T> is a team decision here rather than a lint failure, with nothing to make it stick. TypeScript itself has a preference, printing T[] in inferred types and error messages whatever you wrote, which is a real argument for matching your tools; the argument the other way is everything above, parentheses for union elements, [T] versus T[], and the family resemblance to Set<T> and Map<K, V>.
The whole entry
Run the whole reference suite:
Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/loops.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 14 tests from ./programs/typed-arrays.test.ts
...
running 10 tests from ./programs/typing-arrays.test.ts
one type, two spellings ... ok (246µs)
the brackets bind tightly ... ok (78µs)
a tuple fixes the length and types each position ... ok (19µs)
inference gives you an array, never a tuple ... ok (29µs)
an empty literal accumulates the types you push ... ok (24µs)
a non-empty literal fixes the element type at once ... ok (16µs)
an interface can describe an array, until it cannot ... ok (21µs)
ArrayLike is the shape without the class ... ok (37µs)
reading an element is typed as present, whether or not it is ... ok (273µs)
take readonly array parameters ... ok (26µs)
running 12 tests from ./programs/typing-classes.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...
ok | 692 passed | 0 failed (1s)
Ten tests, and the practice is short. Annotate the variable when you want a tuple, because inference will not give you one, and the error you get instead is about length rather than about types, which is the clue. Prefer Array<T> when the element type has more than one token and T[] when it has one, remembering consistency matters more than the choice and nothing will enforce either. Label tuple positions, which costs nothing and answers the question every reader of [string, number] has. Take readonly array parameters. Turn on noUncheckedIndexedAccess in anything new, and reach for a local or at rather than a length check. And use ArrayLike<T> when you mean array-shaped and readonly T[] when you mean an array you will not change, because they are different promises and only one of them is about mutation.