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>];
+});