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 ✌️.
By the way, you might want to fix that error in there with NonNullable
or Required
;)