What is the process for creating a typed object map using an array of pairs?

Suppose I have an array of pairs as shown below:

const attributes = [
  ['Strength', 'STR'],
  ['Agility', 'AGI'],
  ['Intelligence', 'INT']
  // ..
] as const;

I aim to create a structure like this:

const attributesMap = {
  Strength: {
    name: 'Strength',
    shortName: 'STR',
  },
  Agility: {
    name: 'Agility',
    shortName: 'AGI',
  },
  // ..
};

This structure should be fully typed, ensuring that accessing attributesMap.Strength.shortName will return "STR".

How can we achieve this level of typing?

The approach I've devised so far involves using the following type definition. However, it falls short in properly constraining the shortName property to a specific value:

type mapType = {
  [P in typeof attributes[number][0]]: {
    name: P;
    shortName: typeof attributes[number][1];
  };
};

Answer №1

You're almost there:

type mapType = {
  [P in typeof attributes[number][0]]: {
    name: P;
    // Add Extract function here
    shortName: Extract<typeof attributes[number], readonly [P, unknown]>[1];
  };
};

This code snippet will output:

type mapType = {
    Strength: {
        name: "Strength";
        shortName: "STR";
    };
    Agility: {
        name: "Agility";
        shortName: "AGI";
    };
    Intelligence: {
        name: "Intelligence";
        shortName: "INT";
    };
}

Answer №2

@Coding_Master's response is spot on; I'd like to add a twist by showcasing an approach that leverages TypeScript 4.1's key remapping functionality instead of relying on Extract. Consider this:

const characteristics = [
    ['Strength', 'STR'],
    ['Agility', 'AGI'],
    ['Intelligence', 'INT']
    // ..
] as const;

First, let's assign a name to the type of characteristics for future use, ensuring it only includes numeric literal keys (e.g., 0, 1, 2) and not other array properties like push, pop, etc:

type Traits = Omit<typeof characteristics, keyof any[]>;
/* type Traits = {
    readonly 0: readonly ["Strength", "STR"];
    readonly 1: readonly ["Agility", "AGI"];
    readonly 2: readonly ["Intelligence", "INT"];
} */

With Traits defined, we can now remap using an as clause. For every key I in Traits, Traits[I][0] represents the key/name, while Traits[I][1] corresponds to the shortName:

type MappedTraits = {
    -readonly [I in keyof Traits as Traits[I][0]]: {
        name: Traits[I][0],
        shortName: Traits[I][1]
    }
};

/*
type MappedTraits = {
    Strength: {
        name: "Strength";
        shortName: "STR";
    };
    Agility: {
        name: "Agility";
        shortName: "AGI";
    };
    Intelligence: {
        name: "Intelligence";
        shortName: "INT";
    };
}
*/

(I've also omitted readonly here, but feel free to include it if needed). That's the final outcome you were aiming for!

Playground link for code demo

Answer №3

You can utilize a recursive type structure as shown below:

type MapType<T extends readonly (readonly [string, unknown])[]> = 
    // if the list can be split into a head and a tail
    T extends readonly [readonly [infer A, infer B], ...infer R]
        // these conditions must hold true, but TypeScript may not recognize it
        ? A extends string ? R extends readonly (readonly [string, unknown])[]
            ? {
                // this represents a single property
                [K in A]: {
                    name: A,
                    shortName: B
                }
            // we intersect with the type recursively applied to the tail
            } & MapType<R>
            // these scenarios should never occur
            : never : never
        // base case, return an empty object
        : {};

Playground link

Alternatively, you can create a type that removes the readonly keyword through recursion:

type RemoveReadonly<T> = T extends readonly unknown[] ? {
    -readonly [K in keyof T]: RemoveReadonly<T[K]>;
} : T;

type MapType<T extends [string, unknown][]> = 
    // if the list can be split into a head and a tail
    T extends [[infer A, infer B], ...infer R]
        // these conditions must hold true, but TypeScript may not recognize it
        ? A extends string ? R extends [string, unknown][]
            ? {
                // this represents a single property
                [K in A]: {
                    name: A,
                    shortName: B
                }
            // we intersect with the type recursively applied to the tail
            } & MapType<R>
            // these scenarios should never occur
            : never : never
        // base case, return an empty object
        : {};

Playground link

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

Anonymous function bundle where the imported namespace is undefined

Here is the code snippet I am working with: import * as Phaser from 'phaser'; new Phaser.Game({ width:300, height:300, scale: { mode: Phaser.Scale.FIT, }, type: Phaser.AUTO, scene: { create() {} }, }); Upon compi ...

The React.FC component encountered an invalid hook call

I've encountered an issue while using TypeScript and trying to implement a hook within React.FC. Surprisingly, I received an error message stating that hooks can only be used inside functional components. But isn't React.FC considered a functiona ...

I'm experiencing issues with my TypeScript compiler within my Next.js v14 project

I am working on a project using next.js version 14 and typescript v5. After installing these dependencies, I have noticed that the typescript compiler is not detecting errors related to types as expected. For example, when defining props for a component ...

The combination of TypeScript decorators and Reflect metadata is a powerful tool for

Utilizing the property decorator Field, which adds its key to a fields Reflect metadata property: export function Field(): PropertyDecorator { return (target, key) => { const fields = Reflect.getMetadata('fields', target) || []; ...

Tips on obtaining a unique value that does not already exist in a specific property within an Array of objects

I am faced with a challenge involving an array that contains objects with both source and target properties. My goal is to identify a value that never appears as a target. My current approach seems convoluted. I separate the elements of the array into two ...

What factors contribute to the variations in results reported by Eslint on different machines?

We initially utilized tslint in our project but recently made the switch to eslint. When I execute the command "eslint \"packages/**/*.{ts,tsx}\"" on my personal Windows machine, it detects 1 error and 409 warnings. Surprising ...

Is it possible for TypeScript to automatically detect when an argument has been validated?

Currently, I am still in the process of learning Typescript and Javascript so please bear with me if I overlook something. The issue at hand is as follows: When calling this.defined(email), VSCode does not recognize that an error may occur if 'email ...

What are the steps to implementing MSAL in a React application?

Struggling to integrate the msal.js library with react. Post Microsoft login, redirecting to callback URL with code in the query string: http://localhost:3000/authcallback#code=0.AQsAuJTIrioCF0ambVF28BQibk37J9vZQ05FkNq4OB...etc The interaction.status re ...

Angular 2 - Initiating a function in a child component from its parent

While it's common to send data from a parent component to a child using @Input or call a method on the parent component from the child using @Output, I am interested in doing the opposite - calling a method on the child from the parent. Here is an exa ...

Steps for integrating the ts component into the index.html file

Is there a way to add the ts component in the index.html file? I've been looking for a solution for quite some time now, but haven't had any luck. Can anyone offer any suggestions or help? ...

Receiving an eslint error while trying to integrate Stripe pricing table into a React web application

If you're looking to incorporate a Stripe documentation code snippet for adding a stripe-pricing-table element, here's what they suggest: import * as React from 'react'; // If you're using TypeScript, don't forget to include ...

Experiencing "localhost redirect loop due to NextJS Middleware" error

After successfully integrating email/password authentication to my locally hosted NextJS app using NextAuth, I encountered an issue with the middleware I created to secure routes. Every time I tried to sign out, I received an error stating "localhost redir ...

Using Angular2 to bind HTML markup to a boolean flag and trigger a method when the flag is set

I'm currently developing a solution for Angular 2 Bootstrap Datepicker to automatically close when a user clicks outside of it. My current approach involves tracking external clicks and updating a boolean flag as shown below: @Component({ select ...

What is the best way to manage data types using express middleware?

In my Node.js project, I am utilizing Typescript. When working with Express middleware, there is often a need to transform the Request object. Unfortunately, with Typescript, it can be challenging to track how exactly the Request object was transformed. If ...

Get an angular xml file by utilizing the response from a C# web API download

I am trying to download an XML file from a database using a Web API in C#, which returns the file as byte[]. How can I properly read these bytes and convert them into an XML file on the client side using Angular? Despite attempts with blobs and other metho ...

Enhancing Angular 2 with conditional directives such as #if

Can TypeScript support preprocessor directives similar to #define and #if in C#, especially when working with Angular 2? I am working on a multiplatform project and aiming to use the same code for both mobile and web applications. However, I face challeng ...

Tips for connecting a separate page to a button in Angular?

What is the best way to connect a login component to a button on the home page in Angular, so that it opens up a login page? I have attempted to achieve this by creating a click event for the login button within myaccount-dropdown component in my-account- ...

Developing an attribute in a constructor using Typescript

My javascript code is functioning perfectly: class myController { constructor () { this.language = 'english' } } However, I encountered an issue when trying to replicate the same in Typescript export default class myController { co ...

cdk-virtual-scroll-viewport displays every single element in the list

I am currently utilizing cdk-virtual-scroll-viewport to display a List, but it seems to be rendering all the elements in a similar fashion as a traditional ngFor loop. <cdk-virtual-scroll-viewport itemSize="16"> <div *cdkVirtualFor="let i ...

Retrieve an enumeration from a value within an enumeration

In my coding project, I have an enum called Animals and I've been working on a function that should return the value as an enum if it is valid. enum Animals { WOLF = 'wolf', BADGER = 'badger', CAT = 'cat', } cons ...