From d6702f9c1f87f7f4671b08ed113fb3f0f32ef8cc Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Sun, 9 Apr 2023 16:29:44 -0700 Subject: [PATCH 1/3] add `array-map` for keeping tuple length when using `.map` --- package.json | 5 +++ readme.md | 32 +++++++++++++ src/entrypoints/array-map.d.ts | 15 +++++++ src/entrypoints/recommended.d.ts | 1 + src/tests/array-map.ts | 77 ++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 src/entrypoints/array-map.d.ts create mode 100644 src/tests/array-map.ts diff --git a/package.json b/package.json index bf4035f..9860585 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,11 @@ "types": "./dist/array-index-of.d.ts", "import": "./dist/array-index-of.mjs", "default": "./dist/array-index-of.js" + }, + "./array-map": { + "types": "./dist/array-map.d.ts", + "import": "./dist/array-map.mjs", + "default": "./dist/array-map.js" } }, "keywords": [], diff --git a/readme.md b/readme.md index 68f6a2b..d1d0242 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 4th 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>]; +}); From f1e73928fcb321b30c1e70a924524408bc05f843 Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Thu, 4 May 2023 19:34:36 +0100 Subject: [PATCH 2/3] fix spelling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sindre Bøyum --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index d1d0242..1cac90f 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +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 +- 😭 `array.map` on a tuple loses the tuple length `ts-reset` smooths over these hard edges, just like a CSS reset does in the browser. @@ -300,7 +300,7 @@ const validate = (input: unknown) => { 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. +When you're using `Array.map` with a tuple, the length is lost. This means you lose the guard against accessing an item out of bounds. ```ts // BEFORE From 8b7b7cb21b2b27da7f8ca543c6ab25f89a28f6ef Mon Sep 17 00:00:00 2001 From: Tom Jenkinson Date: Wed, 17 May 2023 07:44:56 +0100 Subject: [PATCH 3/3] docs: fix example --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 1cac90f..6cca389 100644 --- a/readme.md +++ b/readme.md @@ -309,7 +309,7 @@ const tuple = [1, 2, 3] as const; const mapped = tuple.map((a) => a + 1); // oops. There's no 4th element, but no error -console.log(tuple[3]); +console.log(mapped[3]); ``` With `array-map` enabled, this code will now error: @@ -322,7 +322,7 @@ 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]); +console.log(mapped[3]); ``` ## Rules we won't add