typescript-genericstypescript-typesrest-parameters

Typescript generic indexed access type of rest parameters


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


Solution

  • 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