Typescript: derive union type from array of objects



I would like to declare a type-enforced array of items and be able to derive a union type from it. This pattern works if you do not explicitly give a type to the items in the array. I am not sure how to best explain it so here is an example:

EXAMPLE 1

type Pair = {
  key: string;
  value: number;
};

const pairs: ReadonlyArray<Pair> = [
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
] as const;

type Keys = typeof pairs[number]['key']

EXAMPLE 2

type Data = {
  name: string;
  age: number;
};

const DataRecord: Record<string, Data> = {
  foo: { name: 'Mark', age: 35 },
  bar: { name: 'Jeff', age: 56 },
} as const;

type Keys = keyof typeof DataRecord;

Here is an example of deriving the keys when using as const. I want this same behavior but with the array being explicitly typed.

const pairs = [
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
] as const;

type Keys = typeof pairs[number]['key']; // "foo" | "bar"

desired value of keys: "foo"|"bar"

actual value of keys: string

Answer

For a variable you can either let the compiler infer the type from initialization, or write it out explicitly. If you write it explicitly, as you have, then the initialization value is checked against the annotation, but the actual type of the initializer does not affect the type of the variable (so you lose the type information you want). If you let the compiler infer it, it is no longer possible to constrain the type to conform to a specific interface (as you seem to want)

The solution for this is to use a generic function to both constrain the value and infer it’s actual type:

type Pair = {
  key: string;
  value: number;
};
function createPairsArray<T extends readonly Pair[] & Array<{key: V}>, V extends string>(...args: T) {
    return args
}

const pairs = createPairsArray(
  { key: 'foo', value: 1 },
  { key: 'bar', value: 2 },
)

type Keys1 = typeof pairs[number]['key']

type Data = {
  name: string;
  age: number;
};

function createDataObject<T extends Record<string, Data>>(arg: T) {
    return arg;
}
const DataRecord = createDataObject({
  foo: { name: 'Mark', age: 35 },
  bar: { name: 'Jeff', age: 56 },
})

type Keys2 = keyof typeof DataRecord;

Playground Link

Note: For the array case we need to strong arm the compiler a bit into inferring string literal types for key, hence the whole & Array<{key: V}>, where V is a type parameter extending string



Source: stackoverflow