Danila
Danila

Reputation: 18576

How to make typed object map from array of pairs?

Let's say I have an array of pairs like that:

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

And I want to generate something like that:

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

And it should be fully typed, so if I want to access attributesMap.Strength.shortName I will know that it will be "STR"

How this could be typed?

The best I've come up with is this, but it does not really work as intended, because only key is typed as const value, shortName is still union of all the values and I don't know how to make it non union value:

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

Upvotes: 1

Views: 138

Answers (3)

jcalz
jcalz

Reputation: 330571

@A_Blop's answer is correct; I just wanted to follow up with a version that uses TypeScript 4.1's key remapping feature as an alternative to using Extract. Given:

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

Let's give the type of attributes a name we can use later, and to make the remapping easier, let's restrict it so it only has the numeric literal keys (i.e., 0, 1, 2) of each element, and not any of the Array keys like push, pop, etc:

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

Armed with Atts, we can now do the remapping with an as clause. For every key I in Atts, the type Atts[I][0] is the key/name, and the type Atts[I][1] is the shortName:

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

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

(I've also removed readonly but you don't have to do that if you don't want to). And that's the type you wanted!

Playground link to code

Upvotes: 1

A_blop
A_blop

Reputation: 862

You are almost there:

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

results in

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

Upvotes: 3

Aplet123
Aplet123

Reputation: 35560

You can use a recursive type:

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 should always be true but typescript doesn't know that
        ? A extends string ? R extends readonly (readonly [string, unknown])[]
            ? {
                // this'll be just one property
                [K in A]: {
                    name: A,
                    shortName: B
                }
            // we intersect with the type recursed on the tail
            } & MapType<R>
            // these should never happen
            : never : never
        // base case, empty object
        : {};

Playground link

Alternatively, to avoid repeating readonly, you can make a type that recursively gets rid of it:

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 should always be true but typescript doesn't know that
        ? A extends string ? R extends [string, unknown][]
            ? {
                // this'll be just one property
                [K in A]: {
                    name: A,
                    shortName: B
                }
            // we intersect with the type recursed on the tail
            } & MapType<R>
            // these should never happen
            : never : never
        // base case, empty object
        : {};

Playground link

Upvotes: 0

Related Questions