Skip to content

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

Description

@btoo

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.

Activity

  1. AnyhowStep commented on Oct 19, 2019

    @AnyhowStep
    Contributor

    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

  2. AnyhowStep commented on Oct 19, 2019

    @AnyhowStep
    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

  3. btoo commented on Oct 20, 2019

    @btoo
    ContributorAuthor

    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

  4. AnyhowStep commented on Oct 20, 2019

    @AnyhowStep
    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

  5. RyanCavanaugh commented on Oct 30, 2019

    @RyanCavanaugh
    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.

  6. btoo commented on Jun 14, 2024

    @btoo
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions