In this chapter, we examine how Arrays can be typed in TypeScript.
Arrays can play the following roles in JavaScript (either one or a mix of them):
TypeScript accommodates these two roles by offering various ways of typing arrays. We will look at those next.
Array
An Array type literal consists of the element type followed by []
. In the following code, the Array type literal is string[]
:
// Each Array element has the type `string`:
: string[] = ['fee', 'fi', 'fo', 'fum']; const myStringArray
An Array type literal is a shorthand for using the global generic interface type Array
:
: Array<string> = ['fee', 'fi', 'fo', 'fum']; const myStringArray
If the element type is more complicated, we need parentheses for Array type literals:
|string)[]
(number=> boolean)[] (()
The generic type Array
works better in this case:
<number|string>
Array<() => boolean> Array
If the Array has a fixed length and each element has a different, fixed type that depends on its position, then we can use tuple type literals such as [string, string, boolean]
:
: [string, string, boolean] = ['oui', 'sí', true]; const yes
If an interface has only an index signature, we can use it for Arrays:
interface StringArray {: number]: string;
[index
}: StringArray = ['Huey', 'Dewey', 'Louie']; const strArr
An interface that has both an index signature and property signatures, only works for objects (because indexed elements and properties need to be defined at the same time):
interface FirstNamesAndLastName {: number]: string;
[index: string;
lastName
}
: FirstNamesAndLastName = {
const ducks0: 'Huey',
1: 'Dewey',
2: 'Louie',
: 'Duck',
lastName; }
Due to the two roles of Arrays, it is impossible for TypeScript to always guess the right type. As an example, consider the following Array literal that is assigned to the variable fields
:
: Fields = [
const fields'first', 'string', true],
['last', 'string', true],
['age', 'number', false],
[; ]
What is the best type for fields
? The following are all reasonable choices:
type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [
string, string, boolean],
[string, string, boolean],
[string, string, boolean],
[; ]
type Fields = [
string, 'string', boolean],
[string, 'string', boolean],
[string, 'number', boolean],
[; ]
type Fields = [
<string|boolean>,
Array<string|boolean>,
Array<string|boolean>,
Array; ]
When we use non-empty Array literals, TypeScript’s default is to infer list types (not tuple types):
// %inferred-type: (string | number)[]
= [123, 'abc']; const arr
Alas, that’s not always what we want:
function func(p: [number, number]) {
;
return p
}// %inferred-type: number[]
= [1, 2];
const pair1
// @ts-expect-error: Argument of type 'number[]' is not assignable to
// parameter of type '[number, number]'. [...]
func(pair1);
We can fix this by adding a type annotation to the const
declaration, which avoids type inference:
: [number, number] = [1, 2];
const pair2func(pair2); // OK
If we initialize a variable with an empty Array literal, then TypeScript initially infers the type any[]
and incrementally updates that type as we make changes:
// %inferred-type: any[]
= [];
const arr1
.push(123);
arr1// %inferred-type: number[]
;
arr1
.push('abc');
arr1// %inferred-type: (string | number)[]
; arr1
Note that the initial inferred type isn’t influenced by what happens later.
If we use assignment instead of .push()
, things work the same:
// %inferred-type: any[]
= [];
const arr1
0] = 123;
arr1[// %inferred-type: number[]
;
arr1
1] = 'abc';
arr1[// %inferred-type: (string | number)[]
; arr1
In contrast, if the Array literal has at least one element, then the element type is fixed and doesn’t change later:
// %inferred-type: number[]
= [123];
const arr
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
.push('abc'); arr
const
assertions for Arrays and type inferenceWe can suffix an Array literal with a const
assertion:
// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"]
=
const rockCategories 'igneous', 'metamorphic', 'sedimentary'] as const; [
We are declaring that rockCategories
won’t change. That has the following effects:
The Array becomes readonly
– we can’t use operations that change it:
// @ts-expect-error: Property 'push' does not exist on type
// 'readonly ["igneous", "metamorphic", "sedimentary"]'. (2339)
.push('sand'); rockCategories
TypeScript infers a tuple. Compare:
// %inferred-type: string[]
= ['igneous', 'metamorphic', 'sedimentary']; const rockCategories2
TypeScript infers literal types ("igneous"
etc.) instead of more general types. That is, the inferred tuple type is not [string, string, string]
.
Here are more examples of Array literals with and without const
assertions:
// %inferred-type: readonly [1, 2, 3, 4]
= [1, 2, 3, 4] as const;
const numbers1 // %inferred-type: number[]
= [1, 2, 3, 4];
const numbers2
// %inferred-type: readonly [true, "abc"]
= [true, 'abc'] as const;
const booleanAndString1 // %inferred-type: (string | boolean)[]
= [true, 'abc']; const booleanAndString2
const
assertionsThere are two potential pitfalls with const
assertions.
First, the inferred type is as narrow as possible. That causes an issue for let
-declared variables: We cannot assign any tuple other than the one that we used for intialization:
= [1, 2] as const;
let arr
= [1, 2]; // OK
arr
// @ts-expect-error: Type '3' is not assignable to type '2'. (2322)
= [1, 3]; arr
Second, tuples declared via as const
can’t be mutated:
= [1, 2] as const;
let arr
// @ts-expect-error: Cannot assign to '1' because it is a read-only
// property. (2540)
1] = 3; arr[
That is neither an upside nor a downside, but we need to be aware that it happens.
Whenever we access an Array element via an index, TypeScript always assumes that the index is within range (line A):
: string[] = ['Hello'];
const messages
// %inferred-type: string
= messages[3]; // (A) const message
Due to this assumption, the type of message
is string
. And not undefined
or undefined|string
, as we may have expected.
We do get an error if we use a tuple type:
: [string] = ['Hello'];
const messages
// @ts-expect-error: Tuple type '[string]' of length '1' has no element
// at index '1'. (2493)
= messages[1]; const message
as const
would have had the same effect because it leads to a tuple type being inferred.