Reputation: 18576
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
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!
Upvotes: 1
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
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
: {};
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
: {};
Upvotes: 0