Typescript: derive union type from array of objects

The usual approaches are:

  • let TS infer the type of pairs by omitting the explicit type ReadonlyArray<Pair> (see answer)
  • give key in Pair the type "foo"|"bar"

If you don't want to do this, then the only way to infer your keys and restrict the type of pairs is to use a helper function. The Pair type will also be made generic to save the given key string literal types. You can use an IIFE to make the assignment compact:

type Pair<K = string> = {
    key: K;
    value: number;
};

const pairs = (<T>(p: readonly Pair<T>[]) => p)([
    { key: 'foo', value: 1 },
    { key: 'bar', value: 2 },
] as const) // readonly Pair<"foo" | "bar">[]

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

Playground


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