diff --git a/readme.md b/readme.md index 68f6a2b..a8bad0c 100644 --- a/readme.md +++ b/readme.md @@ -7,6 +7,7 @@ TypeScript's built-in typings are not perfect. `ts-reset` makes them better. - 🚨 `.json` (in `fetch`) and `JSON.parse` both return `any` - 🤦 `.filter(Boolean)` doesn't behave how you expect - 😡 `array.includes` often breaks on readonly arrays +- 😭 `array.map` on a tuple looses the tuple length `ts-reset` smooths over these hard edges, just like a CSS reset does in the browser. @@ -293,6 +294,37 @@ const validate = (input: unknown) => { }; ``` +### Keeping the tuple length in resulting tuple from `Array.map` + +```ts +import "@total-typescript/ts-reset/array-map"; +``` + +When you're using `Array.map` with a tuple, the length is lost. This means you loose the guard against accessing an item out of bounds. + +```ts +// BEFORE + +const tuple = [1, 2, 3] as const; +const mapped = tuple.map((a) => a + 1); + +// oops. There's no 3rd element, but no error +console.log(tuple[3]); +``` + +With `array-map` enabled, this code will now error: + +```ts +// AFTER +import "@total-typescript/ts-reset/array-map"; + +const tuple = [1, 2, 3] as const; +const mapped = tuple.map((a) => a + 1); + +// Tuple type 'readonly [number, number, number]' of length '3' has no element at index '3'. +console.log(tuple[3]); +``` + ## Rules we won't add ### `Object.keys`/`Object.entries` diff --git a/src/entrypoints/array-map.d.ts b/src/entrypoints/array-map.d.ts new file mode 100644 index 0000000..5935bde --- /dev/null +++ b/src/entrypoints/array-map.d.ts @@ -0,0 +1,15 @@ +/// + +interface ReadonlyArray { + map( + callbackfn: (value: T, index: number, array: readonly T[]) => U, + thisArg?: any, + ): { [K in keyof this]: U }; +} + +interface Array { + map( + callbackfn: (value: T, index: number, array: T[]) => U, + thisArg?: any, + ): { [K in keyof this]: U }; +} diff --git a/src/entrypoints/recommended.d.ts b/src/entrypoints/recommended.d.ts index 7e6b2b4..22678d0 100644 --- a/src/entrypoints/recommended.d.ts +++ b/src/entrypoints/recommended.d.ts @@ -6,3 +6,4 @@ /// /// /// +/// diff --git a/src/tests/array-map.ts b/src/tests/array-map.ts new file mode 100644 index 0000000..788289c --- /dev/null +++ b/src/tests/array-map.ts @@ -0,0 +1,77 @@ +import { doNotExecute, Equal, Expect } from "./utils"; + +doNotExecute(async () => { + const tuple = [0, 1] as const; + const mapped = tuple.map( + ( + value: (typeof tuple)[number], + index: number, + source: readonly (typeof tuple)[number][], + ) => 1, + ); + + tuple.map(() => 1, {}); + + type tests = [ + Expect>, + Expect>, + ]; + + mapped[0]; + mapped[1]; + // @ts-expect-error + mapped[2]; +}); + +doNotExecute(async () => { + const tuple = [0, 1] as [0, 1]; + const mapped = tuple.map( + ( + value: (typeof tuple)[number], + index: number, + source: (typeof tuple)[number][], + ) => 1, + ); + + tuple.map(() => 1, {}); + + type tests = [ + Expect>, + Expect>, + ]; + + mapped[0]; + mapped[1]; + // @ts-expect-error + mapped[2]; +}); + +doNotExecute(async () => { + const arr: readonly number[] = [0, 1]; + const mapped = arr.map( + ( + value: (typeof arr)[number], + index: number, + source: readonly (typeof arr)[number][], + ) => 1, + ); + + arr.map(() => 1, {}); + + type tests = [Expect>]; +}); + +doNotExecute(async () => { + const arr: number[] = [0, 1]; + const mapped = arr.map( + ( + value: (typeof arr)[number], + index: number, + source: (typeof arr)[number][], + ) => 1, + ); + + arr.map(() => 1, {}); + + type tests = [Expect>]; +});