Skip to content

Commit ee29ef7

Browse files
authored
FixedLengthArray: Fix element type (#1246)
1 parent f999a2c commit ee29ef7

File tree

3 files changed

+142
-23
lines changed

3 files changed

+142
-23
lines changed

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ Click the type names for complete docs.
249249
- [`Join`](source/join.d.ts) - Join an array of strings and/or numbers using the given string as a delimiter.
250250
- [`ArraySlice`](source/array-slice.d.ts) - Returns an array slice of a given range, just like `Array#slice()`.
251251
- [`LastArrayElement`](source/last-array-element.d.ts) - Extracts the type of the last element of an array.
252-
- [`FixedLengthArray`](source/fixed-length-array.d.ts) - Create a type that represents an array of the given type and length.
252+
- [`FixedLengthArray`](source/fixed-length-array.d.ts) - Create a type that represents an array of the given type and length. The `Array` prototype methods that manipulate its length are excluded from the resulting type.
253253
- [`MultidimensionalArray`](source/multidimensional-array.d.ts) - Create a type that represents a multidimensional array of the given type and dimensions.
254254
- [`MultidimensionalReadonlyArray`](source/multidimensional-readonly-array.d.ts) - Create a type that represents a multidimensional readonly array of the given type and dimensions.
255255
- [`ReadonlyTuple`](source/readonly-tuple.d.ts) - Create a type that represents a read-only tuple of the given type and length.

source/fixed-length-array.d.ts

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,89 @@
1+
import type {Except} from './except.d.ts';
2+
import type {TupleOf} from './tuple-of.d.ts';
3+
14
/**
25
Methods to exclude.
36
*/
47
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift';
58

69
/**
7-
Create a type that represents an array of the given type and length. The array's length and the `Array` prototype methods that manipulate its length are excluded in the resulting type.
10+
Create a type that represents an array of the given type and length. The `Array` prototype methods that manipulate its length are excluded from the resulting type.
11+
12+
The problem with the built-in tuple type is that it allows mutating methods like `push`, `pop` etc, which can cause issues, like in the following example:
13+
14+
@example
15+
```
16+
const color: [number, number, number] = [255, 128, 64];
17+
18+
function toHex([r, g, b]: readonly [number, number, number]) {
19+
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
20+
}
821
9-
Please participate in [this issue](https://github.com/microsoft/TypeScript/issues/26223) if you want to have a similar type built into TypeScript.
22+
color.pop(); // Allowed
23+
24+
console.log(toHex(color)); // Compiles fine, but fails at runtime since index `2` no longer contains a `number`.
25+
```
26+
27+
`ArrayLengthMutationKeys` solves this problem by excluding methods like `push`, `pop` etc from the resulting type.
28+
29+
@example
30+
```
31+
import type {FixedLengthArray} from 'type-fest';
32+
33+
const color: FixedLengthArray<number, 3> = [255, 128, 64];
34+
35+
color.pop();
36+
//=> Error: Property 'pop' does not exist on type 'FixedLengthArray<number, 3>'.
37+
```
1038
1139
Use-cases:
1240
- Declaring fixed-length tuples or arrays with a large number of items.
13-
- 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.
1441
- Creating an array of coordinates with a static length, for example, length of 3 for a 3D vector.
1542
16-
Note: This type does not prevent out-of-bounds access. Prefer `ReadonlyTuple` unless you need mutability.
17-
1843
@example
1944
```
20-
import type {FixedLengthArray} from 'type-fest';
45+
let color: FixedLengthArray<number, 3> = [255, 128, 64];
46+
47+
const red = color[0];
48+
//=> number
49+
const green = color[1];
50+
//=> number
51+
const blue = color[2];
52+
//=> number
53+
54+
const alpha = color[3];
55+
//=> Error: Property '3' does not exist on type 'FixedLengthArray<number, 3>'.
56+
57+
// You can write to valid indices.
58+
color[0] = 128;
59+
color[1] = 64;
60+
color[2] = 32;
61+
62+
// But you cannot write to out-of-bounds indices.
63+
color[3] = 0.5;
64+
//=> Error: Property '3' does not exist on type 'FixedLengthArray<number, 3>'.
65+
66+
color.push(0.5);
67+
//=> Error: Property 'push' does not exist on type 'FixedLengthArray<number, 3>'.
2168
22-
type FencingTeam = FixedLengthArray<string, 3>;
69+
color = [0, 128, 255, 0.5];
70+
//=> Error: Type '[number, number, number, number]' is not assignable to type 'FixedLengthArray<number, 3>'. Types of property 'length' are incompatible.
2371
24-
const guestFencingTeam: FencingTeam = ['Josh', 'Michael', 'Robert'];
72+
color.length = 4;
73+
//=> Error: Cannot assign to 'length' because it is a read-only property.
2574
26-
const homeFencingTeam: FencingTeam = ['George', 'John'];
27-
//=> error TS2322: Type string[] is not assignable to type 'FencingTeam'
75+
function toHex([r, g, b]: readonly [number, number, number]) {
76+
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
77+
}
2878
29-
guestFencingTeam.push('Sam');
30-
//=> error TS2339: Property 'push' does not exist on type 'FencingTeam'
79+
console.log(toHex(color)); // `FixedLengthArray<number, 3>` is assignable to `readonly [number, number, number]`.
3180
```
3281
3382
@category Array
34-
@see ReadonlyTuple
3583
*/
36-
export type FixedLengthArray<Element, Length extends number, ArrayPrototype = [Element, ...Element[]]> = Pick<
37-
ArrayPrototype,
38-
Exclude<keyof ArrayPrototype, ArrayLengthMutationKeys>
39-
> & {
40-
[index: number]: Element;
41-
[Symbol.iterator]: () => IterableIterator<Element>;
42-
readonly length: Length;
43-
};
84+
export type FixedLengthArray<Element, Length extends number> =
85+
Except<TupleOf<Length, Element>, ArrayLengthMutationKeys | number | 'length'>
86+
& {readonly length: Length}
87+
& (number extends Length ? {[n: number]: Element} : {}); // Add `number` index signature only for non-tuple arrays.
4488

4589
export {};

test-d/fixed-length-array.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,86 @@
1-
import {expectAssignable, expectNotAssignable} from 'tsd';
1+
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
22
import type {FixedLengthArray} from '../index.d.ts';
33

44
type FixedToThreeStrings = FixedLengthArray<string, 3>;
5+
declare const fixedToThreeStrings: FixedToThreeStrings;
56

67
expectAssignable<FixedToThreeStrings>(['a', 'b', 'c']);
8+
expectAssignable<readonly [string, string, string]>({} as FixedToThreeStrings);
9+
expectAssignable<readonly string[]>({} as FixedToThreeStrings);
10+
11+
// Reading within bounds
12+
expectType<string>({} as FixedToThreeStrings[0]);
13+
expectType<string>({} as FixedToThreeStrings[1]);
14+
expectType<string>({} as FixedToThreeStrings[2]);
15+
16+
// Reading out of bounds
17+
// @ts-expect-error
18+
type OutOfBounds = FixedToThreeStrings[3];
19+
20+
// Writing within bounds
21+
fixedToThreeStrings[0] = 'a';
22+
fixedToThreeStrings[1] = 'b';
23+
fixedToThreeStrings[2] = 'c';
24+
25+
// Writing out of bounds
26+
// @ts-expect-error
27+
fixedToThreeStrings[3] = 'd';
28+
29+
expectType<3>({} as FixedToThreeStrings['length']);
30+
31+
// @ts-expect-error
32+
type NoSplice = FixedToThreeStrings['splice'];
33+
// @ts-expect-error
34+
type NoPush = FixedToThreeStrings['push'];
35+
// @ts-expect-error
36+
type NoPop = FixedToThreeStrings['pop'];
37+
// @ts-expect-error
38+
type NoShift = FixedToThreeStrings['shift'];
39+
// @ts-expect-error
40+
type NoUnshift = FixedToThreeStrings['unshift'];
741

842
expectNotAssignable<FixedToThreeStrings>(['a', 'b', 123]);
943
expectNotAssignable<FixedToThreeStrings>(['a']);
1044
expectNotAssignable<FixedToThreeStrings>(['a', 'b']);
1145
expectNotAssignable<FixedToThreeStrings>(['a', 'b', 'c', 'd']);
46+
47+
type FixedLength = FixedLengthArray<string, number>;
48+
declare const fixedLength: FixedLength;
49+
50+
expectAssignable<FixedLength>({} as string[]);
51+
expectAssignable<readonly string[]>({} as FixedLength);
52+
53+
// Reading
54+
// Note: The extra `undefined` is only present when `noUncheckedIndexedAccess` is enabled.
55+
expectType<string | undefined>(fixedLength[0]);
56+
expectType<string | undefined>(fixedLength[100]);
57+
// Note: Reading directly from the type doesn't include `undefined`.
58+
expectType<string>({} as FixedLength[100]);
59+
60+
// Writing
61+
// This is allowed for now, refer https://github.com/sindresorhus/type-fest/pull/1246#discussion_r2384018774
62+
fixedLength[0] = 'a';
63+
fixedLength[100] = 'b';
64+
65+
// @ts-expect-error
66+
type NoSplice = FixedLength['splice'];
67+
// @ts-expect-error
68+
type NoPush = FixedLength['push'];
69+
// @ts-expect-error
70+
type NoPop = FixedLength['pop'];
71+
// @ts-expect-error
72+
type NoShift = FixedLength['shift'];
73+
// @ts-expect-error
74+
type NoUnshift = FixedLength['unshift'];
75+
76+
expectAssignable<FixedLengthArray<string | number, 3>>(['a', 'b', 'c']);
77+
expectAssignable<FixedLengthArray<string | number, 3>>([3, 2, 1]);
78+
expectAssignable<FixedLengthArray<string | number, 3>>(['a', 'b', 3]);
79+
expectAssignable<FixedLengthArray<string | number, 3>>([1, 'b', 3]);
80+
expectNotAssignable<FixedLengthArray<string | number, 3>>([1, 'b', 3, 4]);
81+
expectNotAssignable<FixedLengthArray<string | number, 3>>([1, 'b', true]);
82+
83+
expectAssignable<FixedLengthArray<string, 2 | 3>>(['a', 'b']);
84+
expectAssignable<FixedLengthArray<string, 2 | 3>>(['a', 'b', 'c']);
85+
expectNotAssignable<FixedLengthArray<string, 2 | 3>>(['a']);
86+
expectNotAssignable<FixedLengthArray<string, 2 | 3>>([1, 2]);

0 commit comments

Comments
 (0)