Array types

Explains CruftScript's array types, Array<T> and the T[] shorthand: how an element type is inferred, why a static index narrows to T but a dynamic one yields T or undefined, how covariant element writes are re-checked, and how an incoming array is validated element by element at a boundary.

An array type describes a variable-length sequence whose elements all share one type. You can write it two ways, and they mean the same thing.

Array<T>    // canonical
T[]         // shorthand

T[] and Array<T> are identical types, so use whichever reads better. Unlike a tuple, an array has no fixed length: it holds zero or more values, each of type T.

Element type and access

When you write an array literal, its element type is the best common type of the members. An empty literal has nothing to infer from, so it is Array<unknown> until context gives it a type.

const xs = [1, 2, 3]     // Array<number>
const empty = []         // Array<unknown>

Indexing behaves differently depending on what the index is. A static integer literal narrows to the element type, because the checker treats it as a direct member access. A dynamic index — a variable, a computed value — could point past the end of the array, so it yields T | undefined, and you must account for the missing case.

const first = (["a", "b"])[0]                            // string
function f(a: Array<string>, i: number) { return a[i] } // string | undefined

What you can assign

Arrays are covariant in their element type: Array<A> is assignable to Array<B> exactly when A is assignable to B. An Array<Dog> fits where an Array<Animal> is expected.

Covariance is normally the source of a well-known unsoundness, because it would let you write the wrong element type through the wider view. CruftScript closes that hole by re-checking every element write against the array's declared element type at the point of the write, so a covariant view cannot be used to smuggle in a bad element.

Values entering from outside

When an array arrives from outside your program, it is validated element by element against T's runtime check before you can treat it as an Array<T>. A partial or mistyped array is rejected at the boundary rather than trusted.

For this to work, T needs a check that can run at runtime. An Array<T> whose element type has no such check is refused rather than accepted on faith, since there would be no honest way to confirm the incoming elements are what they claim to be.

Why it works this way

Covariance is convenient and usually what you want, so CruftScript keeps it and pays for it at the one place it can go wrong: the write. Re-checking writes means you get the flexible reading behavior without the classic array-write unsoundness. Validating incoming arrays element by element extends the same honesty to data from outside your program. An array is only usable as Array<T> once every element has actually been confirmed to be a T, which is why an element type with no runtime check is turned away instead of trusted.

Limitations

  • readonly T[] is an erased modifier. You can write it, but readonly is discarded and does not constrain mutation of the resulting type.
  • In-place element mutation is limited. Writing to an element is admitted only for bounded local arrays. Outside that case, treat an array as read-mostly.
  • An empty literal is Array<unknown>. Its elements are unusable until you narrow them, and it cannot be mutated until its element type is pinned down. Give an empty array an annotation when you need a specific element type.