Typescript generic indexed access type of rest parameters

197 views Asked by At

I am stuck here:

I have an interface Field which has a name and a value of some unknown type.

interface Field<T> {
    name: string;
    value: T;
}

I then have a function form, which takes any amount of Field's as rest parameters and returns the data they hold.

function form<T extends Field<unknown>[]>(
    ...fields: T
): { [k in T[number]['name']]: T[number]['value'] } {
    let data = {};
    fields.forEach((field) => (data[field.name] = field.value));
    return <{ [k in T[number]['name']]: T[number]['value'] }>data;
}

It look like this is action:

const age: Field<number> = { name: 'age', value: 30 };
const sex: Field<string> = { name: 'sex', value: 'men' };

const data = form(age, sex);
// { age: 30, sex: 'men' }

In this example the types of age and sex are just the union of all fields.

data.age; // number | string
data.sex; // number | string

What i want is data to be of type:

const data: { age: number, sex: string } = form(age, sex);

But this returns the error ts(2451).

What does the return type of form need to be. Is this even possible?

(I'm using typescript version 4.9.3, the latest as of 2022-11-29)

[Edit 2022-11-30]: add Link to working example

typescriptlang.org/play

1

There are 1 answers

1
Dimava On BEST ANSWER

Your Field should have a ganegic on its name also

interface Field<K extends string, V> {
  name: K;
  value: V;
}

function ensureField<K extends string, V>(field: Field<K, V>) {
  return field;
}

type FieldListToRecord<List extends Field<any, any>[], O extends {} = {}> =
  | List extends [infer F extends Field<any, any>, ...infer L extends Field<any, any>[]] ?
  FieldListToRecord<L, O & (
    F extends Field<infer K, infer V> ? { [k in K]: V }
    : never
  )>
  : { [K in keyof O]: O[K] }; // <- Pure<T>


function form<T extends Field<any, any>[]>(
  ...fields: T
): FieldListToRecord<T> {
  return Object.fromEntries(
    fields.map(({ name, value }) => [name, value])
  );
}

const age = ensureField({ name: 'age', value: 30 });
//    ^?
const sex = ensureField({ name: 'sex', value: 'men' });
//    ^?

const data = form(age, sex);
//    ^?
// { age: 30, sex: 'men' }

Playground