Search code examples
typescriptmapped-types

How to define Mapped type with a non-optional subset of keys in TypeScript?


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).


Solution

  • 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'>}.

    Playground link