I have this TypeScript code that neatly maps types to their possible values:
export const randomMappings = {
coin: ['heads', 'tails'],
d4: [1, 2, 3, 4],
d6: [1, 2, 3, 4, 5, 6],
d10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
} as const;
export type RandomType = keyof typeof randomMappings;
export type RandomValue<T extends RandomType> = typeof randomMappings[T][number];
// RandomValue<'coin'> is 'heads'|'tails'; RandomValue<'d4'> is 1|2|3|4 and so on.
export type RandomResults = {
[T in RandomType]?: RandomValue<T>;
};
export function pickRandomValues(types: RandomType[]): RandomResults {
return Object.fromEntries(
types.map((type) => [
type,
randomMappings[type][Math.floor(Math.random() * randomMappings[type].length)],
]),
);
}
I would like to enforce that the type returned by pickRandomValues
is a map that contains only the keys present in the argument types
instead of having all possible keys as optional.
For example:
const x = pickRandomValues(['coin', 'd6']);
console.log(x.coin, x.d6); //works
console.log(x.d10); //should fail because d10 is not present in x
I think the way RandomValue<T>
is defined is quite elegant, so I would not like to change that if possible.
Is there any way to do that? I would not mind changing the types
argument to a map instead of an array so I can do something like {[T in keyof typeof types]: RandomValue<T>}
, but what I tried doesn't work because if I want it to allow to pass an object with just the keys I am interested in the type would be the same as RandomResults
stands now (all keys optional).
You can make pickRandomValues
generic over the values in types
:
export function pickRandomValues<T extends RandomType>(
types: T[]
): {[K in T]: RandomValue<K>} {
return Object.fromEntries(
types.map((type) => [
type,
randomMappings[type][Math.floor(Math.random() * randomMappings[type].length)],
]),
// needs type assertion
) as {[K in T]: RandomValue<K>};
}
const coin = pickRandomValues(['coin'])
coin.coin // 'heads' | 'tails'
coin.d4 // error
const coinD4 = pickRandomValues(['coin', 'd4'])
coinD4.coin // 'heads' | 'tails'
coinD4.d4 // 1 | 2 | 3 | 4
coinD4.d10 // error
For example, in pickRandomValues(['coin', 'd4'])
, T
would be 'coin' | 'd4'
and the return type would be {coin: RandomValue<'coin'>, d4: RandomValue<'d4'>}
.