我怎样才能让打字稿推断出泛型对象数组中参数的值?

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

我正在创建一个简单的类似模式的类型来使用 React 动态生成表。

这些是我写的类型:

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
};

这就是我期望使用架构的方式:

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}
    ],
};

然后我有一些 React 组件接受值 T 和模式 MySchema<T>

底层(React)代码工作得很好,但我找不到正确地将 value 作为类型 T[key] 的字段的方法。有没有办法做我想做的事,或者我应该对模式进行不同的建模?

我们需要一种方法让 TypeScript 以某种方式进行关联(知道键与值的类型相关联)。让我们把键的并集变成元组类型,模仿 Object.keys.

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;

我们有模式 T、假定的键 TKeys 和结果 R。 遍历键,为每个键创建一个字段,并为该字段使用正确的值类型。这基本上是映射键并“返回”一个值。

因为这个类型returns是一个元组,我们不能直接使用它。我们必须先得到元组元素的类型,我们可以用 [number] 来做,然后用 [] 从中得到一个数组,结果是这个特殊的代码:

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

这似乎在操场上有效

Playground

p.s。肯定有更好更高效的方法;这只是我想到的第一个

对于 [=17= 中的每个 K,您希望 MySchema<T>fields 属性 是 MySchemaField<T, K>union ].也就是说,您想 分布 MySchemaField<T, K> K.

中的工会

有多种方法可以做到这一点。我在这里的方法可能是制作一个分布式对象类型(如microsoft/TypeScript#47109), where you make a mapped type and immediately index into it中创造的那样)。像{[K in KS]: F<K>}[KS]这样的东西最终会成为[=21=的联合] 对于 KS 中的每个 K。在你的情况下 KSkeyof T,而 F<K>MySchemaField<T, K>。像这样:

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

让我们看看它是如何工作的:

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

这就是你想要的,对吧?现在一切都按照您的意愿进行推断:

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