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

Should the literal number value types of const-asserted arrays' indices be inferred? #34589

Closed
4 tasks done
btoo opened this issue Oct 19, 2019 · 6 comments
Closed
4 tasks done
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@btoo
Copy link

btoo commented Oct 19, 2019

Search Terms

array tuple const assertion literal index number

Suggestion, Use Cases, and Examples

I would like to infer the index type of a const-asserted array to be that of its position in the array, rather than just number (i.e. I want the literal number type). However, I can't seem to create a way to extract this into a type, even though I know TypeScript is holding onto this information somewhere.

For example, this code follows my intuition:

const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const;

/** "Mo" | "Tu" */
type FirstTwoDays = (typeof days)[1 | 2]

/** 7 */
type DaysCount = typeof days.length;

const impossibleDayIndex = 88;
/** Tuple type 'readonly [...]' of length '7' has no element at index '88' */
type CorrectlyErrors = (typeof days)[typeof impossibleDayIndex];

Yet here, index is of type number. Although this is understandable, given the signature of Array['forEach'], it begs the question - how could I assert, as the as const does, that index should be of type 0 | 1 | 2 | 3 | 4 | 5 | 6?

days.forEach((day, index) => {});

Likewise, how could I get this to "work"?

const indexTypeLost = days.indexOf('Tu')
/** number, rather than 2 */
type IndexTypeLost = typeof indexTypeLost

The closest I can get to achieving my desired type is with

/** number | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "length" | "toString" | ... */
type IndicesOfDays = keyof typeof days;

but it's not perfect:

const convertIndexToDay = (index: IndicesOfDays) => days[index]
convertIndexToDay(impossibleDayIndex) // incorrectly does not throw an error

Checklist

My suggestion meets these guidelines:

  • [?] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 19, 2019

Partial workaround for indexOf,

const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const;

/** "Mo" | "Tu" */
type FirstTwoDays = (typeof days)[1 | 2]

/** 7 */
type DaysCount = typeof days.length;

type PopFront<ArrT extends readonly any[]> =
    ((...arr: ArrT) => void) extends ((_: any, ...tail: infer TailT) => void)?
    TailT :
    never
;

type PushFront<TailT extends readonly any[], HeadT> =
    ((head : HeadT, ...arr: TailT) => void) extends ((...arr: infer ArrT) => void)?
    ArrT :
    never
;

type IndexOf<
    ArrT extends readonly any[],
    ValueT extends ArrT[number],
    IndexTHack extends readonly any[] = [],
    ResultT extends number = never
> =
    {
        0: number,
        1: ResultT,
        2: IndexOf<
            PopFront<ArrT>,
            ValueT,
            PushFront<IndexTHack, any>,
            ResultT|(ValueT extends ArrT[0] ? IndexTHack["length"] : never)
        >
    }[
        number extends ArrT["length"] ?
        0 :
        ArrT["length"] extends 0 ?
        1 :
        2
    ]
;

declare function indexOf<ArrT extends readonly any[], ValueT extends ArrT[number]>(
    arr: ArrT,
    value : ValueT
): IndexOf<ArrT, ValueT>;

//2
const tu = indexOf(days, 'Tu' as const)

//2|3
const tuWe = indexOf(days, 'We' as 'Tu' | 'We')

//0|5
const suFr = indexOf(days, 'Su' as 'Su'|'Fr')

Playground

@AnyhowStep
Copy link
Contributor

For "number keys",

const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'] as const;

/** "Mo" | "Tu" */
type FirstTwoDays = (typeof days)[1 | 2]

/** 7 */
type DaysCount = typeof days.length;

type PopFront<ArrT extends readonly any[]> =
    ((...arr: ArrT) => void) extends ((_: any, ...tail: infer TailT) => void)?
    TailT :
    never
;

type PushFront<TailT extends readonly any[], HeadT> =
    ((head : HeadT, ...arr: TailT) => void) extends ((...arr: infer ArrT) => void)?
    ArrT :
    never
;

type NumberKeysOf<
    ArrT extends readonly any[],
    IndexTHack extends readonly any[] = [],
    ResultT extends number=never
> =
    {
        0: number,
        1: ResultT,
        2: NumberKeysOf<
            PopFront<ArrT>,
            PushFront<IndexTHack, any>,
            ResultT|IndexTHack["length"]
        >
    }[
        number extends ArrT["length"] ?
        0 :
        ArrT["length"] extends 0 ?
        1 :
        2
    ]
;

//type k = 0 | 1 | 2 | 3 | 4 | 5 | 6
type k = NumberKeysOf<typeof days>;

Playground

@btoo
Copy link
Author

btoo commented Oct 20, 2019

Damn. That was an astoundingly clever usage of spread function params as a vehicle for recursively building an array! Tried it out myself and it indeed works. Then I was reminded of a talk (i think) anders hejlsberg had where he mentioned TypeScript having some internal recursion limit, and I was able to confirm: for me,NumberKeysOf breaks down for an array as const at around the length of 42 (intelligently defaulting to number because Type instantiation is excessively deep and possibly infinite)

I guess even if it had worked, it would've been a masterly albeit roundabout way of achieving what I thought would be a lightweight inference task. As you can see I am not a TypeScript expert lol

@AnyhowStep
Copy link
Contributor

There are ways to work around the depth limit but those are a bit out of scope =x

I agree it would be nice to get TS to just give us the number literal keys of a tuple without hackery.

A nicer way to type indexOf() would be nice, too

The workarounds above are... Not great

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Oct 30, 2019
@RyanCavanaugh
Copy link
Member

This one is kind of problematic and would need a bunch of special handling, or new type system primitives we currently don't have.

Fundamentally, keyof is an operation that returns string literal types, because that's how JS properties work. But forEach gives numeric values to the index argument, and obviously only includes the array indices themselves.

@btoo
Copy link
Author

btoo commented Jun 14, 2024

getting the literal number types for as const arrays is a little easier with template literal types

https://x.com/mattpocockuk/status/1801625713272893750

playground link

@btoo btoo closed this as completed Jun 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants