Skip to content

Conversation

@som-sm
Copy link
Collaborator

@som-sm som-sm commented Sep 25, 2025

Fixes #1245

Updated the implementation of FixedLengthArray. The updated implementation simply calls BuildTuple and then removes keys like 'pop', 'push' from it.

Notes:

  • Removed the third type argument ArrayPrototype from FixedLengthArray. This was used for internal computation and shouldn't have been exposed, so it's more of a bug than a breaking change, and since it had a default type, it's safe to remove it.
  • Added number to ArrayLengthMutationKeys, so that out-of-bounds access is prevented. I can't think of a case where this would break something.

@som-sm som-sm requested a review from sindresorhus September 25, 2025 15:14
@som-sm som-sm force-pushed the fix/fixed-length-array branch from 82cd904 to fc2891f Compare September 25, 2025 15:15
@som-sm
Copy link
Collaborator Author

som-sm commented Sep 25, 2025

Also, IMO, it doesn't feel very useful if we can only generate tuples having the same type in all positions. Maybe in the next major version we should change the API to simply accept a tuple, like:

export type FixedLengthArray<TArray> = Except<TArray, ArrayLengthMutationKeys>;

If I understand correctly, the only purpose of this type is to remove methods like 'push', 'pop' from a tuple, right?

@sindresorhus
Copy link
Owner

Should these tests pass?

expectAssignable<string[]>({} as FixedLengthArray<string, 3>);
declare const a: FixedLengthArray<string, 3>;
declare const i: number;
expectType<string>(a[i]);

@sindresorhus
Copy link
Owner

A few more tests to add:

expectType<3>({} as FixedLengthArray<string, 3>['length']);
expectType<string>({} as FixedLengthArray<string, 3>[number]);

@sindresorhus
Copy link
Owner

Also, IMO, it doesn't feel very useful if we can only generate tuples having the same type in all positions. Maybe in the next major version we should change the API to simply accept a tuple, like:

That sounds more like a different type. This type is homogeneous while your suggestion is heterogeneous. Could maybe be:

export type FixedTuple<T extends unknown[]> = Except<T, ArrayLengthMutationKeys>;

?

@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

That sounds more like a different type. This type is homogeneous while your suggestion is heterogeneous. Could maybe be:

True, but now that we have BuildTuple available, we can create homogeneous tuples simply by using FixedTuple and BuildTuple, like:

type RGB = FixedTuple<BuildTuple<3, number>>

So, does it still make sense to have two separate types?

@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

Should these tests pass?

expectAssignable<string[]>({} as FixedLengthArray<string, 3>);

This shouldn't, because string[] has methods like 'push', 'pop', while FixedLengthArray<string, 3> does not. This is similar to why readonly string[] is not be assignable to string[]. Added the following test case instead:

expectAssignable<readonly string[]>({} as FixedLengthArray<string, 3>);

declare const a: FixedLengthArray<string, 3>;
declare const i: number;
expectType<string>(a[i]);

This one is related to the following point:

  • Added number to ArrayLengthMutationKeys, so that out-of-bounds access is prevented. I can't think of a case where this would break something.

You can't index the array using the non-literal number type, because number could be something out-of-bounds. Like, in your example, i could hold a value of 10 at runtime.


A few more tests to add:

expectType<string>({} as FixedLengthArray<string, 3>[number]);

This test is similar to the above. LMK if you see any issues with removing number.

@sindresorhus
Copy link
Owner

So, does it still make sense to have two separate types?

Yeah, discoverability. FixedTuple is more of a building block.

@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

  • Added number to ArrayLengthMutationKeys, so that out-of-bounds access is prevented. I can't think of a case where this would break something.

I'll add a note regarding this in the doc.

@som-sm som-sm marked this pull request as draft September 26, 2025 07:26
@sindresorhus
Copy link
Owner

How about this?

expectType<string | undefined>({} as FixedLengthArray<string, 3>[number]);

@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

How about this?

expectType<string | undefined>({} as FixedLengthArray<string, 3>[number]);

Yeah, I was also thinking about this. I think this is better. So, instead of removing the number index signature, let's make it readonly. This way, we could still read from out-of-bounds indices but not write to them.

This also helps when we create non-tuples using FixedLengthArray, because if we remove the number index signature, then we would not be able to access any index from something like FixedLengthArray<string, number>.

@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

Yeah, I was also thinking about this. I think this is better. So, instead of removing the number index signature, let's make it readonly. This way, we could still read from out-of-bounds indices but not write to them.

Turns out it's not that straightforward.

  1. If we add the number index signature with an extra | undefined, like:

    export type FixedLengthArray<Element, Length extends number> =
        Except<BuildTuple<Length, Element>, ArrayLengthMutationKeys | number>
        & {readonly [x: number]: Element | undefined};

    Then the following test cases break:

    expectAssignable<readonly [string, string, string]>({} as FixedToThreeStrings);
    expectAssignable<readonly string[]>({} as FixedToThreeStrings);
    image
  2. If we just make the number index signature readonly w/o adding the extra | undefined, like:

    export type FixedLengthArray<Element, Length extends number> =
        Except<BuildTuple<Length, Element>, ArrayLengthMutationKeys | number>
        & {readonly [x: number]: Element};

    Then out-of-bounds accesses have the extra | undefined only when noUncheckedIndexedAccess is enabled.

    // @noUncheckedIndexedAccess: true
    const outOfBounds = fixedToThreeStrings[3];
    //=> string | undefined
    // @noUncheckedIndexedAccess: false
    const outOfBounds = fixedToThreeStrings[3];
    //=> string

    And also, if we directly index from the type, the result never has the extra | undefined irrespective of the noUncheckedIndexedAccess setting. This seems to be a TS quirk, I was expecting noUncheckedIndexedAccess to affect type-level accesses as well.

    type OutOfBounds = FixedToThreeStrings[3];
    //=> string

So, I have come up with a hybrid approach wherein we don't add the number index signature for tuples, so out-of-bounds accesses simply error, and for non-tuples (which should be a rarer use case) we add the number index signature similar to point no. 2.

@som-sm som-sm force-pushed the fix/fixed-length-array branch from a197bbd to 8c789eb Compare September 26, 2025 11:22
@som-sm som-sm force-pushed the fix/fixed-length-array branch from 8c789eb to 728c4f4 Compare September 26, 2025 11:24
Use-cases:
- Declaring fixed-length tuples or arrays with a large number of items.
- Creating a range union (for example, `0 | 1 | 2 | 3 | 4` from the keys of such a type) without having to resort to recursive types.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a range union (for example, 0 | 1 | 2 | 3 | 4 from the keys of such a type) without having to resort to recursive types.

Not sure what this means, should we remove this?

In fact feels like we can completely remove the use-cases section, it doesn't add enough value IMO.

Declaring fixed-length tuples or arrays with a large number of items.

Feels like the primary use-case is to create arrays with "large number of items", which isn't true.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it was there to suggest it could be used for something like this:

type IndexRange<L extends number> =
	Exclude<keyof FixedLengthArray<unknown, L>, keyof any[]>;

but we can drop it. not that useful.

@som-sm som-sm marked this pull request as ready for review September 26, 2025 11:38
@som-sm
Copy link
Collaborator Author

som-sm commented Sep 26, 2025

@sindresorhus Made significant changes to this PR, also updated the JSDoc completely, please review.

@sindresorhus sindresorhus mentioned this pull request Sep 27, 2025
3 tasks
@som-sm som-sm marked this pull request as draft September 27, 2025 15:55
@som-sm
Copy link
Collaborator Author

som-sm commented Sep 27, 2025

Updated the PR, here's a quick summary:

Length Behaviour Reading Writing
Within bounds Out of bounds Within bounds Out of bounds
Literal Updated Allowed Errors Allowed Errors
Current
Allowed
(🐛 Adds | undefined if noUncheckedIndexedAccess is ON)
🐛 Allowed
(Adds | undefined depending on noUncheckedIndexedAccess)
Allowed 🐛 Allowed
Non Literal Updated
Allowed
(Adds | undefined depending on noUncheckedIndexedAccess)
Allowed
(Adds | undefined depending on noUncheckedIndexedAccess)
Allowed Allowed
Current
Allowed
(Adds | undefined depending on noUncheckedIndexedAccess)
Allowed
(Adds | undefined depending on noUncheckedIndexedAccess)
Allowed Allowed
🐛 Represents bugs

@som-sm som-sm marked this pull request as ready for review September 27, 2025 17:07
@sindresorhus sindresorhus merged commit ee29ef7 into main Sep 27, 2025
6 checks passed
@sindresorhus sindresorhus deleted the fix/fixed-length-array branch September 27, 2025 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

can't destructure FixedLengthArray correctly

3 participants