Determine the data type of an index within an array declaration

Imagine we have the following array literal:

const list = ['foo', 'bar', 'baz'] as const;

We are attempting to create a type that represents potential indices of this array. This is what we tried:

const list = ['foo', 'bar', 'baz'] as const;

type ListIndex = Exclude<keyof (typeof list), keyof []>;

However, the ListIndex ends up being "0" | "1" | "2" instead of 0 | 1 | 2.

Does anyone know how to achieve the desired 0 | 1 | 2 type?

Answer №1

One possible solution is to utilize the below type:

type Index<T extends readonly any[]> = Exclude<Partial<T>["length"], T["length"]>

We can now test it using list:

type Test = Index<typeof list> // Test = 0 | 1 | 2 // successful!

Link to live code demo

Answer №2

There is no straightforward method to achieve this, which can be quite frustrating. While there are ongoing efforts to address this issue, the current workaround involves a bit of extra work.

In my own project, I have implemented the following solution:

/** Define the type of elements in an array */
type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer E)[] ? E : never;

/** Function signature with arguments for 'Tail' */
type AsFunctionWithArgsOf<T extends unknown[] | readonly unknown[]> = (...args: T) => any;

/** Extract arguments from the function signature for 'Tail' */
type TailArgs<T> = T extends (x: any, ...args: infer T) => any ? T : never;

/** Obtain elements of an array after the first element */
type Tail<T extends unknown[] | readonly unknown[]> = TailArgs<AsFunctionWithArgsOf<T>>;

/** Internal utility for 'IndicesOf'; likely not needed outside this context */
type AsDescendingLengths<T extends unknown[] | readonly unknown[]> =
    [] extends T ? [0] :
    [ElementOf<ElementOf<AsDescendingLengths<Tail<T>>[]>>, T['length']];

/** Union of numeric literals representing possible indices of a tuple */
type IndicesOf<T extends unknown[] | readonly unknown[]> =
    number extends T['length'] ? number :
    [] extends T ? never :
    ElementOf<AsDescendingLengths<Tail<T>>>;

By using this implementation, your code can be structured as follows:

const list = ['foo', 'bar', 'baz'] as const;

type ListIndex = IndicesOf<typeof list>;

The underlying concept here revolves around examining the `T['length']` property (where `type T = typeof list`) to obtain actual numbers that correlate with the indices. The extracted number will always be one greater than the highest index in the array. By iteratively removing elements from the array and checking the length each time, we can determine all the indices present. This recursive process relies on the definitions of types like `Tail`, `ElementOf`, and `AsDescendingLengths`.

To elaborate further, `Tail` represents the same tuple type without its initial element, akin to a 'Pop'. Due to TypeScript's limited functionality with tuples, we utilize a function-based approach for manipulating them effectively. Functions like `ElementOf` and `Tail` serve other purposes beyond this specific scenario. However, caution must be exercised with deeply nested tuples, as TypeScript may avoid recursive types for performance reasons.

Although certain constructs like `AsDescendingLengths` may seem tailored for this particular use case, these defined types could potentially find relevance in broader contexts. Including these definitions in a global `.d.ts` file eliminates the need for individual imports but might clutter the overall scope of the project.

An additional application example is demonstrated below:

interface Array<T> {
    /**
     * Returns the index of the first occurrence of a value in an array.
     * @param searchElement The value to locate in the array.
     * @param fromIndex The array index at which to begin the search.
     * If `fromIndex` is omitted, the search starts at index 0.
     * Returns -1 if `searchElement` is not found.
     */
    indexOf<This extends T[], T>(this: This, searchElement: T, fromIndex?: IndicesOf<This>): -1 | IndicesOf<This>;
}

interface ReadonlyArray<T> {
    /**
     * Returns the index of the first occurrence of a value in an array.
     * @param searchElement The value to locate in the array.
     * @param fromIndex The array index at which to begin the search.
     * If `fromIndex` is omitted, the search starts at index 0.
     * Returns -1 if `searchElement` is not found.
     */
    indexOf<This extends readonly T[], T>(this: This, searchElement: T, fromIndex?: IndicesOf<This>): -1 | IndicesOf<This>;
}

Answer №3

The defined outcome is what you receive. However, we can accomplish your desired result with a clever mapping technique:

const list = ['foo', 'bar', 'baz'] as const;

type StrNum = {
  "0": 0,
  "1": 1,
  "2": 2,
  "3": 3,
  "4": 4 // add more values if needed
}

type MapToNum<A extends keyof StrNum> = {
  [K in A]: StrNum[K]
}[A] // this converts string keys to numbers using the StrNum type

type ListIndex = MapToNum<Exclude<keyof (typeof list), keyof []>>;
// resulting type will be 0 | 1 | 2

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for effectively utilizing the drag and drop feature with the Table Component in Ant Design

Recently, I received a new project from another team, and my client is requesting some changes to the admin page. Specifically, they want to customize the order of data pulled from the database. For instance, they would like to arrange the job positions in ...

How to update an Array<Object> State in ReactJS without causing mutation

In my program, I store an array of objects containing meta information. This is the format for each object. this.state.slotData [{ availability: boolean, id: number, car: { RegistrationNumber : string, ...

Angular 2: Musing on the potential of Hot Module Replacement and the power of @ngrx/store

If you're just getting started, this link might be helpful: understanding the purpose of HMR. When it comes to managing and designing large projects, I'm still in the early stages and haven't grown a wise beard yet. So, I'm seeking adv ...

Tips for clearing out text in a <p> tag with javascript when it exceeds a specific length of 100 characters

Is there a way to automatically truncate text to (...) when it exceeds 100 characters inside a paragraph with the class .class? For instance, if I have a long paragraph like this: <div class='classname'> <p>Lorem ipsum dolor sit ame ...

Changes are reflected in the service variable, but they are not updating in the component

I am facing an issue with a variable that is supposed to track the progress of image uploads. The variable seems to be working fine, but it remains undefined in my component. Upload Service method uploadProfilePic(url:string, user_id:string, image:any) { ...

The specified file ngx-extended-pdf-viewer/assets/pdf.js cannot be found

I have integrated the ngx-extended-pdf-viewer package in my Angular application using npm to enable the display of PDF content. According to the setup instructions, I have added the following configuration in my angular.json file: "assets": [ ...

The table is failing to display the values contained within the array

My users list is successfully retrieved from the API and I can see the data in my console. However, when I attempt to map it and display it as a table, it doesn't seem to work as expected. This is the component I'm working with: interface IUser { ...

Angular - Set value only if property is present

Check if the 'rowData' property exists and assign a value. Can we approach it like this? if(this.tableObj.hasOwnProperty('rowData')) { this.tableObj.rowData = this.defVal.rowData; } I encountered an error when attempting this, specif ...

Restricting access to tabPanel in tabView when a tab is clicked using Angular

In my tabview, I have multiple tabpanels and I am able to programmatically control it using the [activeIndex] property. However, I am facing an issue where I want to display an alert and deny access to a specific tab if a certain variable is set to false. ...

Challenges arise when employing reduce with data types in an object

I want to transform an object in a function so that all keys are converted from Camel case to Pascal case. My Declaration: export interface INodeMailerResponseLower { accepted: string[]; rejected: string[]; envelopeTime: number; messageTim ...

The mat-slide-toggle updates the values for all products, with each value being unique

In my app, I am using Material slide-toggle to control the activation status of products. However, I am facing the following issues: Whenever I toggle one product, it affects the values of all other products as well. The displayed value does not match t ...

Welcome to Digital Ocean! It appears you are attempting to utilize TypeScript

Over the past 48 hours, I've been struggling to build my project on a DO App. Strangely enough, there were no changes made to the project, yet out of nowhere it started breaking with an unexpected error message: [2023-04-06 09:03:02] │ It seems like ...

Implement a new functionality to a publicly accessible type without violating the pre-established agreement

I'm looking to enhance an existing exported type with a new method, without causing any disruption to the current usage in production. import * as BunyanLogger from 'bunyan'; import init from './logger'; export type Logger = Bunya ...

Is there a way to declare the different types of var id along with its properties in Typescript?

I recently received a task to convert a JavaScript file to a TypeScript file. One issue I am currently facing is whether or not I should define types for the 'id' with this expression, e.g., id={id}. So far, I have tried: Even though I defined ...

Handling a change event for a mat-datepicker in Angular 7 can be tricky, especially when its value is tied to an optional input parameter. Let's dive into how to

I've recently started working with angular development and encountered a challenge with a mat-datepicker. The value of the datepicker is connected to filterDate using [(ngModel)] as an @Input() parameter. I have implemented a handleChange event that e ...

Dynamically loading classes in TypeScript without using default export

Is there a way to dynamically load classes in TypeScript without using a default export method? I have managed to make it work partly, but I am looking for a solution that doesn't require a default export: export default class Test extends Base { ... ...

The outcome of a promise is an undefined value

I am facing an issue where I need to access the result of my API call outside the promise, but the value I receive is always undefined. Within the OrderService : public async getOrderPrice(device: string) : Promise<any> { this.urlOrderPrice = th ...

Is there a way to dynamically alter the fill color of an SVG component using props along with tailwindcss styling?

Having a bit of trouble cracking this code puzzle. I've got a logo inside a component, but can't seem to pass the className fill options correctly. This is all happening in a NextJS environment with NextUI and tailwind css. const UserLogo = (prop ...

Tips for resolving the AWS CDK setContext error while updating beyond v1.9.0

Attempting to upgrade AWS CDK code from version 1.9.0 to version 1.152.0, I encountered a problem with the setContext code no longer being valid. The error message states ‘Cannot set context after children have been added: Tree’ The original code tha ...

The type 'number' cannot be assigned to the type 'Element'

Currently, I am developing a custom hook called useArray in React with TypeScript. This hook handles array methods such as push, update, remove, etc. It works perfectly fine in JavaScript, but encounters errors in TypeScript. Below is the snippet of code f ...