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

Array .at() on readonly tuples #100

Open
Sheraff opened this issue Mar 6, 2023 · 3 comments
Open

Array .at() on readonly tuples #100

Sheraff opened this issue Mar 6, 2023 · 3 comments

Comments

@Sheraff
Copy link

Sheraff commented Mar 6, 2023

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.

This is how it is currently:

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"
@Sheraff
Copy link
Author

Sheraff commented Mar 6, 2023

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

@mattpocock
Copy link
Collaborator

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.

@unional
Copy link

unional commented May 14, 2023

FYI here is the implementation in type-plus:
https://github.com/unional/type-plus/blob/main/ts/array/array.ts#L17

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants