`StrictArray`

Similar to Array but with a stricter API that reduces the likelihood of "footguns".

The main features are:

  • The constructor only has 1 behavior: use args as elements. No length.
  • Empty slots don't exist.
  • Accessing with non-Uint32 numbers will throw RangeErrors (and TypeErrors, depending on value) rather than implicit coercion (-0 is also considered invalid)
  • Out-of-bounds access is also a hard error.
  • There's a static method withLen (inspired by Rust) that creates a StrictArray of length arg[0] with a filler value of arg[1]. This makes the length mandatory and the filler optional (default undefined)

Here's my draft implementation (warning: link will be broken when I move the Gist to a proper tc39-template repo)

My problem with introducing a new array type, is that creates a schism in the data-types we use. If I'm making a library, and I want an array to be passed in as an argument, what should I do?

  • Only support the old array type, because that's most prevalent and easy-to-create (any time you use the array literal syntax [...], you'd get the old-style array)
  • Only support the new array type, because that's the safer one to use? Though this may require end-users of my API to auto-convert this old-style arrays to this new-style one. I myself may be required to convert the new-style back to old-style when calling other third-party library functions. So, lots of conversions would be happening.
  • I could support both styles, but then I, as a library author, don't get to reap any of the benefits of the new-style array - I still have to always assume that out-of-bounds access does not throw an error, etc.

So we do have to weigh the pros of adding this new kind of Array with the major con of this kind of divide between people using the two styles.

I also feel like a number of these points could be done by updating our existing array class with new helper functions, instead of making a new array type altogether. For example:

  • We already have Array.of() that you can always use instead of the Array constructor.
  • Perhaps we could discuss adding some sort of .strictAt(index) function that requires the index to be an in-bound UInt32 (I sort of doubt they'd actually add this, because the precedence has always been to coerce, and to provide undefined when the thing does not exist, but still, if we were to add this functionality, I think it would be better on our existing Array class instead of in a new one).
  • The withLen() function sounds like a nice-to-have that isn't necessarily related to strict arrays, and could be a proposal of its own that we just add to the Array class?

Your second point about not allowing empty slots in an array would be nice to have, but perhaps that too could be solved by adding features to the original Array class, such as: An assertNoEmptySlots() methods you can call on an array whenever you want, to cause it to throw an error if the array has empty slots. You can call this on arrays that enter your library, and then make sure you don't do anything silly that would cause empty slots to be added to that array. A function like this would actually be really nice to have.

3 Likes

I totally agree with that! Now I think it's better to just add "safety methods" to the "classic" array.

BTW, if we were to (hypothetically) introduce StrictArray, 2-way conversions would be ergonomic:

const classic = [0,1];//Array(0,1);
const safe = new StrictArray(0,1);

// convert to safe
new StrictArray(...classic);
// notice how the constructor plays the role of a literal

// convert to classic
[...safe];
/*
my draft doesn't implement iterators, yet,
but let's assume it does.
*/

// convert to safe, strictly
StrictArray.from(classic);
// again, my draft doesn't have this.
/*
I'll define what it does:
0. if Array.isArray returns false, throw error.
1. if "array-like" has empty slots, throw error.
2. return "array-like" converted via constructor.
*/

I understand that not everything is about ergonomics, I just wanted to explain the hypothetical scenario

1 Like

FWIW, this pattern can fail with larger array sizes because many JS engines have a max limit to the number of arguments that can be passed.

For example the JavaScriptCore engine has a hard-coded argument limit of 65536.

That's unfortunate. I thought 2^16 was too low, considering array.length has a max value of 2^32-1, but then I read about the stack-limit and everything made sense.

Maybe future engine versions could conditionally switch to the heap at the implementation-level? That would be more spec-compliant