Background
In Typescript, arrays don't have a size when initialised. Therefore, if you try to access an element of the array, it can be undefined:
const arrayImplicit = [item1, item2];
// ❌ possibly undefined
arrayImplicit[1].type;
const arrayExplicit: MyItem[] = [item1, item2];
// ❌ possibly undefined
arrayExplicit[1].type;
These show up as errors if you turn on the option noUncheckedIndexedAccess
.
Now, there are a couple of ways to overcome that. You can cast the array with as const
when initialising, you can use a tuple, or you can use a non-null assertion operator when accessing:
const arrayWithCast = [item1, item2] as const;
// ✅ all good
arrayWithCast[1].type;
const tupleExplicit: [MyItem, MyItem] = [item1, item2];
// ✅ all good
tupleExplicit[1].type;
// works, but annoying to work with and not a good practice
arrayImplicit[1]!.type;
arrayExplicit[1]!.type;
Problem
However, tuples become cumbersome to work with when your array contains many elements. My use case is I am defining an array of mock data. This can contain dozens of elements, so I don't want to maintain a large tuple like this:
const mockUsers: [MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser, MockUser] = ...
And casting an array as const only works for arrays with an implicitly inferred type. If you try to cast an array with an explicitly defined type (const array: MyItem[]
), there is a type mismatch. If you use a readonly type (const array: readonly MyItem[]
), the type mismatch is gone, but the elements are still possibly undefined:
// ❌ The type 'readonly [{ type: string; name: string; }, { type: string; name: string; }]' is 'readonly'
// and cannot be assigned to the mutable type 'MyItem[]'.
const arrayExplicitWithCast: MyItem[] = [item1, item2] as const;
const arrayExplicitWithCastReadonly: readonly MyItem[] = [item1, item2] as const;
// ❌ possibly undefined
arrayExplicitWithCastReadonly[0].type;
Finally, I don't want to use an implicit array like const array = [item1, item2, ...] as const;
. If I do that, then typescript won't prevent me from inserting incorrect items into the array. Since these objects can contain many fields, and it is important that all the required fields be present, I need typescript to ensure that I only insert instances of MyItem
:
const array = [
item1,
item2,
// ❌ this should be an error:
{ some: "other", stuff: 12 },
] as const;
Live example
Check the full example in the typescript playground.
Question
How can I make it work without using tuples, and without having to use the non-null assertion operator (!
), but still preserving the type-checks for each element in the array?
The array is immutable, I only use it to populate the mock queries or assert values in tests.