Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ReadonlyArray] Add types for .at() #101

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions readme.md
Expand Up @@ -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`
Expand Down
18 changes: 18 additions & 0 deletions src/entrypoints/array-at.d.ts
@@ -0,0 +1,18 @@
/// <reference path="utils.d.ts" />

interface ReadonlyArray<T> {
at<
const N extends number,
I extends number = `${N}` extends `${infer J extends number}.${number}`
? J
: N,
>(
index: N,
): TSReset.Equal<I, number> extends true
? T | undefined
: `${I}` extends `-${infer J extends number}`
? `${TSReset.Subtract<this["length"], J>}` extends `-${number}`
? undefined
: this[TSReset.Subtract<this["length"], J>]
: this[I];
}
1 change: 1 addition & 0 deletions src/entrypoints/recommended.d.ts
Expand Up @@ -2,5 +2,6 @@
/// <reference path="filter-boolean.d.ts" />
/// <reference path="is-array.d.ts" />
/// <reference path="json-parse.d.ts" />
/// <reference path="array-at.d.ts" />
/// <reference path="array-includes.d.ts" />
/// <reference path="set-has.d.ts" />
22 changes: 22 additions & 0 deletions src/entrypoints/utils.d.ts
Expand Up @@ -14,4 +14,26 @@ declare namespace TSReset {
: T extends symbol
? symbol
: T;

type BuildTuple<L extends number, T extends any[] = []> = T extends {
length: L;
}
? T
: BuildTuple<L, [...T, unknown]>;

// 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> = A extends number
? B extends number
? BuildTuple<A> extends [...infer U, ...BuildTuple<B>]
? U["length"]
: never
: never
: never;

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
type NotEqual<X, Y> = true extends Equal<X, Y> ? false : true;
}
60 changes: 60 additions & 0 deletions 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<Equal<typeof a, false>>,
Expect<Equal<typeof b, 1>>,
Expect<Equal<typeof c, "2">>,
Expect<Equal<typeof d, undefined>>,
Expect<Equal<typeof e, 1>>,
];
});

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<Equal<typeof a, "2">>,
Expect<Equal<typeof b, 1>>,
Expect<Equal<typeof c, false>>,
Expect<Equal<typeof d, undefined>>,
Expect<Equal<typeof e, "2">>,
];
});

doNotExecute(async () => {
const arr = [false, 1, "2"] as const;

const index = 0 as 0 | 1

const a = arr.at(index);
type tests = [Expect<Equal<typeof a, false | 1>>];
});

doNotExecute(async () => {
const arr = [false, true, 1, "2"] as const;

const index = -1 as -1 | -2

const a = arr.at(index);
type tests = [Expect<Equal<typeof a, "2" | 1>>];
});

doNotExecute(async () => {
const arr = [false, 1, "2"] as const;
const index = 1 as number;
const a = arr.at(index);
type tests = [Expect<Equal<typeof a, false | 1 | "2" | undefined>>];
});