Array is another fundamental JavaScript data type which represents a heterogeneous collection (collection which can contain more than one data-type inside). Hegel provides syntax for annotating array types and restricts collection element types.
const numbers: Array<number> = [1, 2, 3];
If you are familiar with TypeScript or Flow.js, you may know about array types, but both Flow.js and TypeScript provide two methods of array definition: via Array-constructor and via adding
[]
(square brackets) at the end of the type name:
const oneWay: Array<number> = [1, 2, 3];const anotherOne: number[] = [1, 2, 3];
Hegel provides only the first variant without additional syntax with square brackets.
Array Constructor
There are two ways to define an array in JavaScript:
const arrayLiteral = [];const arrayConstructorInvocationWithLength = new Array(0);
Hegel treats these differently. When you create an array via the Array()
constructor - items of the array will be "undefined" and will have "undefined" type in addition to the annotated one.
const arrayLiteral: Array<number> = [];arrayLiteral[0] = 4; // 👌!// Error: Type "undefined" is incompatible with type "number"arrayLiteral[1] = undefined;const arrayInstance = new Array<number>(4);arrayInstance[0] = 4; // 👌!arrayInstance[1] = undefined; // 👌!
The reason for this is the behavior of the Array constructor. Even if you set the length of your array it will be filled with undefined, so Hegel can't give any guarantees that all your elements will have the annotated type.
TypeScript and Flow.js do not handle this condition, so it's easy to get runtime TypeErrors with these analyzers TypeScript Example, Flow.js Example:
const arrayInstance = new Array<number>(4);let intSequence = "";for (const num of arrayInstance) {// TypeError: Cannot read property 'toFixed' of undefinedintSequence += num.toFixed(0);}
Getting element from Array
Another interesting Hegel "feature" is type inference for elements which retrieved from an array by index syntax.
const numbers: Array<number> = [];// Type of firstElement is "number | undefined"const firstElement = numbers[0];
If you are familiar with TypeScript or Flow.js, these "analyzers" will infer
firstElement
asnumber
which can create a TypeError at runtime. TypeScript Example, Flow.js example.
const numbers: Array<number> = [];// TypeError: Cannot read property 'toFixed' of undefinedconst firstElement = numbers[0].toFixed();
Let's go back to Hegel. This behavior also means that even if you try to access an element in a for loop you still get "number | undefined" element type. You must specify that your value is not undefined using type refinement.
const numbers: Array<number> = [1, 2, 3];for (let i = 0; i < numbers.length; i++) {const num = numbers[i]; // still "number | undefined"if (typeof num === "number") {// ...}}
The reason for this decision is the mutable nature of the "length" property of Array, so I can write the following code, which will break my program without this property behavior. TypeScript example, Flow.js Example
const numbers: Array<number> = [1, 2, 3];let intSequence = "";numbers.length = 5;for (let i = 0; i < numbers.length; i++) {// TypeError: Cannot read property 'toFixed' of undefinedintSequence += numbers[i].toFixed();}
Why not make length 'readonly' and add refinement for the
length
array property?". The answer is a popular method of cleaning an array. In many JavaScript programs you may see the next line:
someArray.length = 0;
This is the most popular way to quickly clean an array.
Nowadays this is not the best practice for iterating over an array. You can use forEach
(or other array methods like map
, filter
and etc.) as a safe variant of iterating over the array.
const numbers: Array<number> = [1, 2, 3];// num type is "number"numbers.forEach((num) => {});
Subtyping
Another safe part of Hegel Arrays is their invariant nature. You can't assign one array to another if they contain different element types:
const numbers: Array<number> = [1, 2, 3];// Error: Type "Array<number>" is incompatible with type "Array<number | string>"const numbersOrStrings: Array<number | string> = numbers;
The reason for this decision is the reference nature of JavaScript arrays. If Hegel allows this assignment you will be able mutate the source array via another and get unpredictable errors.
const numbers: Array<number> = [1, 2, 3];const numbersOrStrings: Array<number | string> = numbers;numbersOrStrings.push("some string");// TypeError: num.toFixed is not a functionconst fixedNumbers = numbers.map((num) => num.toFixed(0));
If you are familiar with TypeScript, you may know about this problem. TypeScript Example