Keegan 82
Keegan 82

Reputation: 404

Typescript template literal type from object array

const source = [
  { group: 'first', key: 'A' },
  { group: 'first', key: 'B' },
  { group: 'second', key: 'A' }
] as const;

type DerivedGroupKeys = `${typeof source[number]['group']}.${typeof source[number]['key']}`;
//gives "first.A" | "first.B" | "second.A" | "second.B"
type HardCodedGroupKeys = 'first.A' | 'first.B' | 'second.A';
//gives "first.A" | "first.B" | "second.A"

I would like the DerivedGroupKeys to be the same as HardCodedGroupKeys (without hardcoding). I am almost there but it is giving me every possible combination of group and key rather than just the combinations defined in the array. Is this possible?

Upvotes: 1

Views: 1000

Answers (2)

0Valt
0Valt

Reputation: 10345

This behavior is expected, to quote the handbook:

For each interpolated position in the template literal, the unions are cross multiplied:

What you want is possible, but involves a couple of extra steps. If you think about it, when you index a tuple with number, the resulting type can be any of the types of tuple members (hence, the union). What you need is to narrow the index type to a numeric literal: if you index a tuple with a literal N, the resulting type can only be the type of the Nth member of the tuple (hence, no union).

First, we can get a union of the indices of the tuple:

type Indices<A> = Exclude<keyof A, keyof any[]>;

Next, simply create a mapped type from tuple with "keys" as tuple indices and "values" as the desired output. Finally, just index the mapped type with tuple indices:

type DerivedGroupKeys = { [ I in Indices<typeof source> ] : `${typeof source[I]['group']}.${typeof source[I]['key']}` }[Indices<typeof source>];

Upvotes: 2

Lesiak
Lesiak

Reputation: 25936

The docs of Template Literal Types say that:

When a union is used in the interpolated position, the type is the set of every possible string literal that could be represented by each union member:

To overcome this issue, you can map each array element separately, instead of mapping the union.

const source = [
  { group: 'first', key: 'A' },
  { group: 'first', key: 'B' },
  { group: 'second', key: 'A' }
] as const;

type ToLiteral<T> = 
  T extends {group: infer G, key: infer K} ? 
    G extends string ? 
      K extends string ? `${G}.${K}` : never 
    : never 
  : never;

type SourceElemsType = typeof source[number]
type SourceElesLiterals = ToLiteral<SourceElemsType>

Upvotes: 2

Related Questions