In this blog post, we examine how Arrays can be typed in TypeScript.
Arrays are used in the following two roles in JavaScript (and sometimes a mix of the two):
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[]
:
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];
An Array type literal is a shorthand for using the global generic interface type Array
:
const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];
If the element type is more complicated, then you need to put them in parentheses:
(number|string)[]
(() => boolean)[]
In this case, I prefer Array
:
Array<number|string>
Array<() => boolean>
Both array type literals and Array
require all elements to have the same type. That’s how we know that they are for Arrays-as-lists.
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]
:
const yes: [string, string, boolean] = ['oui', 'sí', true];
If an interface has only an index signature, we can use it for Arrays:
interface StringArray {
[index: number]: string;
}
const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];
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 {
[index: number]: string;
lastName: string;
}
const ducks: FirstNamesAndLastName = {
0: 'Huey',
1: 'Dewey',
2: 'Louie',
lastName: 'Duck',
};
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
:
const fields = [
['first', 'string', true],
['last', 'string', true],
['age', 'number', false],
];
What is the best type for fields
? The following are all reasonable options:
type Fields1 = Array<[string, string, boolean]>;
type Fields2 = Array<[string, ('string'|'number'), boolean]>;
type Fields3 = Array<Array<string|boolean>>
type Fields4 = [
[string, string, boolean],
[string, string, boolean],
[string, string, boolean],
];
type Fields5 = [
[string, 'string', boolean],
[string, 'string', boolean],
[string, 'number', boolean],
];
type Fields6 = [
Array<string|boolean>,
Array<string|boolean>,
Array<string|boolean>,
];
// Etc.
When we use non-empty Array literals, TypeScript’s default is to infer list types (not tuple types):
// %inferred-type: number[]
const arr = [123];
Alas, that’s not always what we want:
type Pair = [string, boolean];
// %inferred-type: (str: string) => (string | boolean)[]
const callback = (str: string) => [str, Boolean(str)];
// @ts-ignore: Type '(string | boolean)[][]' is not assignable to
// type 'Pair[]'.
// Type '(string | boolean)[]' is missing the following properties
// from type 'Pair': 0, 1 (2322)
const pairs: Array<Pair> = ['hello', '', '!!!'].map(callback);
The inferred type for callback
should be a tuple type. It isn’t and that’s why its results don’t match the type parameter Pair
of Array<>
in the last line.
We can fix this by specifying the return type of callback
explicitly (instead of relying on type inference):
const callback2 = (str: string): [string, boolean] =>
[str, Boolean(str)];
const pairs2: Array<Pair> = ['hello', '', '!!!'].map(callback2);
We also could have used Pair
instead of [string, boolean]
for the result of callback2
.
If we initialize a variable via an empty Array literal, then TypeScript initially infers the type any[]
and incrementally updates that type as we make changes:
const arr1 = [];
// %inferred-type: any[]
arr1;
arr1.push(123);
// %inferred-type: number[]
arr1;
arr1.push('abc');
// %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 = [];
arr1[0] = 123;
// %inferred-type: number[]
arr1;
arr1[1] = 'abc';
// %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[]
const arr = [123];
// @ts-ignore: Argument of type '"abc"' is not assignable to
// parameter of type 'number'. (2345)
arr.push('abc');
const
assertions for Arrays and type inference We 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 categoriesOfRocks
won’t change – which has the following effects:
The Array becomes readonly
: We can’t use operations that change it:
// @ts-ignore: Property 'push' does not exist on type
// 'readonly ["igneous", "metamorphic", "sedimentary"]'. (2339)
rockCategories.push('sand');
TypeScript infers a tuple. Compare:
// %inferred-type: string[]
const rockCategories2 = ['igneous', 'metamorphic', 'sedimentary'];
TypeScript infers literal types ("igneous"
etc.) instead of more general types. That is, the inferred tuple type is not [string, string, string]
.
Here are two more examples of const
assertions:
// %inferred-type: number[]
const numbers1 = [1, 2, 3, 4];
// %inferred-type: readonly [1, 2, 3, 4]
const numbers2 = [1, 2, 3, 4] as const;
// %inferred-type: (string | boolean)[]
const booleanAndString1 = [true, 'abc'];
// %inferred-type: readonly [true, "abc"]
const booleanAndString2 = [true, 'abc'] as const;