diff --git a/package.json b/package.json index cee2108..206e76b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,11 @@ "import": "./dist/fetch.mjs", "default": "./dist/fetch.js" }, + "./array-at": { + "types": "./dist/array-at.d.ts", + "import": "./dist/array-at.mjs", + "default": "./dist/array-at.js" + }, "./array-includes": { "types": "./dist/array-includes.d.ts", "import": "./dist/array-includes.mjs", diff --git a/readme.md b/readme.md index 65aa2c3..3a03e71 100644 --- a/readme.md +++ b/readme.md @@ -262,6 +262,33 @@ const validate = (input: unknown) => { }; ``` +### Make `.at()` on `as const` arrays more smart + +```ts +import "@total-typescript/ts-reset/array-at"; +``` + +When you're using `.at()` on a tuple, you lose the specificity of your array's type + +```ts +// BEFORE + +const array = [false, 1, "2"] as const +const first = array.at(0) // false | 1 | "2" | undefined +const last = array.at(-1) // false | 1 | "2" | undefined +``` + +With `array-at` enabled, you keep the type of the specific index you're accessing: + +```ts +// AFTER +import "@total-typescript/ts-reset/array-at"; + +const array = [false, 1, "2"] as const +const first = array.at(0) // false +const last = array.at(-1) // "2" +``` + ## Rules we won't add ### `Object.keys`/`Object.entries` diff --git a/src/entrypoints/array-at.d.ts b/src/entrypoints/array-at.d.ts new file mode 100644 index 0000000..ae1afb8 --- /dev/null +++ b/src/entrypoints/array-at.d.ts @@ -0,0 +1,18 @@ +/// + +interface ReadonlyArray { + at< + const N extends number, + I extends number = `${N}` extends `${infer J extends number}.${number}` + ? J + : N, + >( + index: N, + ): TSReset.Equal extends true + ? T | undefined + : `${I}` extends `-${infer J extends number}` + ? `${TSReset.Subtract}` extends `-${number}` + ? undefined + : this[TSReset.Subtract] + : this[I]; +} diff --git a/src/entrypoints/recommended.d.ts b/src/entrypoints/recommended.d.ts index 6eabca1..26f40bb 100644 --- a/src/entrypoints/recommended.d.ts +++ b/src/entrypoints/recommended.d.ts @@ -2,5 +2,6 @@ /// /// /// +/// /// /// diff --git a/src/entrypoints/utils.d.ts b/src/entrypoints/utils.d.ts index 00171cb..6ec23aa 100644 --- a/src/entrypoints/utils.d.ts +++ b/src/entrypoints/utils.d.ts @@ -14,4 +14,26 @@ declare namespace TSReset { : T extends symbol ? symbol : T; + + type BuildTuple = T extends { + length: L; + } + ? T + : BuildTuple; + + // Extra `A extends number` and `B extends number` needed for union types to work Such as Subtract<10 | 20, 1> + type Subtract = A extends number + ? B extends number + ? BuildTuple extends [...infer U, ...BuildTuple] + ? U["length"] + : never + : never + : never; + + type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y + ? 1 + : 2 + ? true + : false; + type NotEqual = true extends Equal ? false : true; } diff --git a/src/tests/array-at.ts b/src/tests/array-at.ts new file mode 100644 index 0000000..22a5d10 --- /dev/null +++ b/src/tests/array-at.ts @@ -0,0 +1,60 @@ +import { doNotExecute, Equal, Expect } from "./utils"; + +doNotExecute(async () => { + const arr = [false, 1, "2"] as const; + + const a = arr.at(0); + const b = arr.at(1); + const c = arr.at(2); + const d = arr.at(3); + const e = arr.at(1.5); + type tests = [ + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + ]; +}); + +doNotExecute(async () => { + const arr = [false, 1, "2"] as const; + + const a = arr.at(-1); + const b = arr.at(-2); + const c = arr.at(-3); + const d = arr.at(-4); + const e = arr.at(-1.5); + type tests = [ + Expect>, + Expect>, + Expect>, + Expect>, + Expect>, + ]; +}); + +doNotExecute(async () => { + const arr = [false, 1, "2"] as const; + + const index = 0 as 0 | 1 + + const a = arr.at(index); + type tests = [Expect>]; +}); + +doNotExecute(async () => { + const arr = [false, true, 1, "2"] as const; + + const index = -1 as -1 | -2 + + const a = arr.at(index); + type tests = [Expect>]; +}); + +doNotExecute(async () => { + const arr = [false, 1, "2"] as const; + const index = 1 as number; + const a = arr.at(index); + type tests = [Expect>]; +});