Home > Enterprise >  Mapped type producing unexpected results when created from generic type
Mapped type producing unexpected results when created from generic type

Time:04-14

I'm using static objects to describe contract definitions, and attempting to generate types based on them. These contracts can optionally have some options, which I want to describe using an array of option names. I then want to use this array to construct a type for passing in option values. All options should be required. See the following code:

const definition  = { optionNames: ['a', 'b'] as const }

type OptionNames = typeof definition['optionNames'][number]
// type OptionNames = "a" | "b"

type Options = Record<OptionNames, string>
// type Options = {
//   a: string;
//   b: string;
// }

So far so good, I can construct the Options type statically without any issues. However when I try to do the same with generic types, I'm getting some unexpected results:

interface Definition { optionNames?: readonly string[] }

type ConstructOptions<T1 extends Definition, T2 = T1['optionNames'][number]> = T2 extends string | number | symbol ? Record<T2, string> : {}

type GenericOptions = ConstructOptions<typeof definition>
// type GenericOptions = Record<"a", string> | Record<"b", string>

I would expect GenericOptions to be the same as Options, however the result is a union of Records which each only have 1 of the options as a required key. Why is this happening?

CodePudding user response:

Use a tuple to make sure that each constituent is not considered separately:

type ConstructOptions<T1 extends Definition, T2 = T1['optionNames'][number]> = [T2] extends [string | number | symbol] ? Record<T2, string> : {}

Full example:

const definition  = { optionNames: ['a', 'b'] as const }

interface Definition { optionNames?: readonly string[] }

type ConstructOptions<T1 extends Definition, T2 = T1['optionNames'][number]> = [T2] extends [string | number | symbol] ? Record<T2, string> : {}

type GenericOptions = ConstructOptions<typeof definition>;

A weird thing you would probably never think of, but it works. Just TypeScript things here ✌️.

Playground

By the way, you might want to fix that error in there with NonNullable or Required ;)

  • Related