ts-reset
ts-reset copied to clipboard
Array `.at()` on readonly tuples
Unless there is a good reason for typescript not to return the correct type when accessing a tuple with .at
it feels like a good candidate for this library.
const a = [false, 1, '2'] as const
const b = a.at(0)
// ^? const b: false | 1 | "2" | undefined
const c = a.at(-1)
// ^? const c: false | 1 | "2" | undefined
But with this library it could be:
const a = [false, 1, '2'] as const
const b = a.at(0)
// ^? const b: false
const c = a.at(-1)
// ^? const c: "2"
As long as the index is positive, the following works pretty well:
interface ReadonlyArray<T> {
at<I extends number>(index: I): this[I];
}
We can detect (and extract from) negative numbers with something like:
interface ReadonlyArray<T> {
at<I extends number>(index: I): `${I}` extends `-${infer J extends number}` ? T | undefined : this[I];
}
But at that point I'm only typing it as T | undefined
(which is the default implementation, meaning "the type of any member of the tuple"). I wish we could do something with J
.
We can bring in a classic bit of array length maths:
type Length<T extends any[]> = T extends { length: infer L } ? L : never
type BuildTuple<L extends number, T extends any[] = []> = T extends { length: L } ? T : BuildTuple<L, [...T, any]>
type Subtract<A extends number, B extends number> = BuildTuple<A> extends [...(infer U), ...BuildTuple<B>]
? Length<U>
: never
interface ReadonlyArray<T> {
at<I extends number> (index: I): `${I}` extends `-${infer J extends number}`
? this[Subtract<this["length"], J>]
: this[I]
}
Now we're able to type the negative indexes too, but only as long as they're smaller than the length, so we need to detect a negative number again:
interface ReadonlyArray<T> {
at<I extends number> (index: I): `${I}` extends `-${infer J extends number}`
? `${Subtract<this["length"], J>}` extends `-${number}`
? undefined
: this[Subtract<this["length"], J>]
: this[I]
}
And here's the final result:
const a = [false, 1, '2'] as const
const b = a.at(0)
// ^? const b: false
const c = a.at(-1)
// ^? const c: "2"
const d = a.at(-4)
// ^? const d: undefined
Hmmmm, I think I'm happy with the current workaround, which is to just use a[0]
- I think the overhead isn't necessarily worth it here. I'll leave this open in case others disagree.
FYI here is the implementation in type-plus: https://github.com/unional/type-plus/blob/main/ts/array/array.ts#L17