Home > Software engineering >  How can I let typescript infer the value of a parameter in an array of generic objects?
How can I let typescript infer the value of a parameter in an array of generic objects?

Time:03-15

I'm creating a simple schema-like type to dynamically generate tables using React.

Those are the types that I wrote:

type MySchemaField<T, K extends keyof T> = {
    key: K;
    label: React.ReactNode;
    render?: React.ComponentType<{item: T, value: T[K]}>;
};
type MySchema<T> = {
    fields: MySchemaField<T, keyof T>[]; // <-- here is the problem: I don't want to specify the parameter K here because it depends on the single field
};

And this is how I'm expecting to use the schema:

type MyModel = {
  name: string;
  slug: string;
  location: {address: string, country: string};
};

const schema: MySchema<MyModel> = {
    fields: [
        {key: 'name', label: 'Name'},
        {key: 'slug', label: 'Link', render: ({value}) => value &&
            <a href={`/my-path/${value}`} target="_blank" rel="noreferrer">{value}</a> || null}, // value should be inferred as string
        {key: 'location', label: 'Address', render: ({value}) => <>{value.country}</>, // value should be {address, country}
    ],
};

I then have some React components accepting the values T and the schema MySchema<T>.

The underlying (React) code works just fine but I cannot find a way to correctly have value as a field of the type T[key]. Is there a way to do what I want or should I model differently the schema?

CodePudding user response:

You want the fields property of MySchema<T> to be a union of MySchemaField<T, K> for every K in keyof T. That is, you want to distribute MySchemaField<T, K> across unions in K.

There are different ways to do this. My approach here would probably be to make a distributive object type (as coined in microsoft/TypeScript#47109), where you make a mapped type and immediately index into it. Something like {[K in KS]: F<K>}[KS] will end up being a union of F<K> for every K in KS. In your case KS is keyof T, and F<K> is MySchemaField<T, K>. Like this:

type MySchema<T> = {
    fields: Array<{ [K in keyof T]-?: MySchemaField<T, K> }[keyof T]>;
};

Let's see how that works:

type MySchemaMyModel = MySchema<MyModel>
/* type MySchemaMyModel = {
    fields: (
      MySchemaField<MyModel, "name"> | 
      MySchemaField<MyModel, "slug"> | 
      MySchemaField<MyModel, "location">
    )[];
} */

That's what you want, right? And now everything is inferred as you desire:

const schema: MySchema<MyModel> = {
    fields: [
        { key: 'name', label: 'Name' },
        {
            key: 'slug', label: 'Link', render: ({ value }) => value &&
            <a href={`/my-path/${value}`} target="_blank" rel="noreferrer">{value}</a> 
            || null
        },
        { key: 'location', label: 'Address', render: ({ value }) => 
          <>{value.country}</>, }
    ],
};

Playground link to code

CodePudding user response:

We need a way for TypeScript to associate (know that a key is associated with the value's type) in some way. Let's turn the union of keys into a tuple type, mimicking Object.keys.

How to transform union type to tuple type

type CreateFields<T, Keys, R extends ReadonlyArray<unknown> = []> =
    Keys extends [] // if no keys
        ? R         // then we are done
        : Keys extends [infer Key, ...infer Rest]
            ? CreateFields<T, Rest, [...R, {
                key: Key;
                label: React.ReactNode;
                render?: Key extends keyof T ? React.ComponentType<{ item: T; value: T[Key] }> : never;
            }]>
            : never;

We have the schema T, the supposed keys of T, Keys, and the result R. Loop through the keys, creating a field for each, with the correct value type for the field. This is basically mapping over the keys and "returning" a value.

Because this type returns a tuple, we can't use it directly. We have to get the type of the elements of the tuple first, which we can do with [number], and then make an array from that with [], resulting in this peculiar code:

type MySchema<T> = {
    fields: CreateFields<T, TuplifyUnion<keyof T>>[number][];
};

And this seems to work in the playground

Playground

p.s. there is definitely a better and more efficient way; this is just the first I came up with

  • Related