使用 属性 和通用类型获取 "keyof" 中的项目类型

Get the type of an item in "keyof" using a property and generic types

我想定义一个通用类型的接口,它必须接受一个对象,其键作为“根字段名称”,值作为对象数组,该对象数组定义一些子字段,键作为名称子字段和类型作为字段值的类型。 像这样:

interface Inputs{
    emails: { email: string, active: boolean, i: number }[]
}

const obj:Inputs = {emails: [ {email: "...", active: true, i: 100} ]}

将此作为通用类型接收的接口有一个“名称”属性,它将接收子字段的(keyof)名称(例如 active )和一个具有参数的函数接收名称 属性.

中定义的子字段的类型

像这样

    [
      {
        name: "active",
        component: ({ value, values }) => {
          console.log(value, values);
          return <>Component</>;
        }
      }
    ]

在此示例中,值必须将“boolean”作为唯一可接受的类型,因为对象中的活动键具有布尔类型。

我想做的几乎都做到了。唯一的问题是,函数的参数不是接收子字段的确切类型,而是获取对象所有类型的联合。

所以在前面的例子中,由于“email”是字符串类型,所以value应该是string类型,而不是接收类型是string |编号 |布尔值(对象中的所有可用类型)。

不知道自己有没有讲清楚,不过我准备了一个sandbox来做的更好

https://codesandbox.io/s/boring-fast-pmmhxx?file=/src/App.tsx

interface Options<
  T extends { [key: string]: unknown }[],
  Key extends keyof T[number]
> {
  values: T;
  value: Key;
}

interface InputDef<
  T extends { [key: string]: any }[],
  Key extends keyof T[number]
> {
  name: Key;
  component: (props: Options<T, T[number][Key]>) => React.ReactNode;
}

interface Props<T extends { [key: string]: [] }, Key extends keyof T> {
  name: Key;
  inputs: InputDef<T[Key], keyof T[Key][number]>[];
  callback: (values: T) => void;
}

interface Inputs {
  firstName: string;
  lastName: string;
  emails: { email: string; active: boolean; other: number }[];
}

const GenComponent = <T extends { [key: string]: any }, Key extends keyof T>({
  name,
  inputs
}: Props<T, Key>) => {
  console.log(inputs);
  return (
    <div>
      {name} {JSON.stringify(inputs)}
    </div>
  );
};

interface MainComponentProps {
  callback: TestCallback<Inputs>;
}

const MainComponent: React.FC<MainComponentProps> = ({ callback }) => {
  return (
    <>
      <GenComponent
        callback={callback}
        name="emails"
        inputs={[
          {
            name: "active",
            component: ({ value, values }) => {
              console.log(value, values);
              return <>Component</>;
            }
          }
        ]}
      />
    </>
  );
};

type TestCallback<Data> = (values: Data) => void;

function test<Data>(values: Data): void {
  console.log(values);
}

export default function App() {
  return (
    <div className="App">
      <MainComponent callback={test} />
    </div>
  );
}

在第 57 行,由于对象中的名称是“active”,因此值的类型应该是“boolean”而不是“string | number | boolean”。 我怎样才能做到这一点?

谢谢!

我将简化您的示例以说明问题出在哪里以及如何解决。首先,你有一个泛型 KeyValFunc<T, K> 类型,它接受一个对象类型 T 和一个如果它的键类型 K,并保存该键和一个接受类型匹配的值的函数该键的对象类型 属性:

interface KeyValFunc<T, K extends keyof T> {
    key: K,
    valFunc: (val: T[K]) => void
}

所以对于这个界面

interface Foo {
    x: number,
    y: string,
    z: boolean
}

你可以写一个 KeyValFunc<Foo, "x"> 类型的值,它的 key"x" 类型,它的 valFunc(val: number) => void 类型:

const obj: KeyValFunc<Foo, "x"> = { key: "x", valFunc: val => val.toFixed() }; // okay

一切都很好,但是现在您想要一个 KeyValFunc<T, K> 的数组,用于一些给定的 T 但您不关心具体的 K 是什么,并且在事实上 K 每个数组元素都可以不同。也就是说,您想要一个 异构数组 类型。您的想法是将其写为 KeyValFunc<T, keyof T>[]:

type KeyValFuncArray<T> = KeyValFunc<T, keyof T>[];

但是,不幸的是,这不起作用:

const arr: KeyValFuncArray<Foo> = [
    { key: "x", valFunc: val => val.toFixed() } // error!
    // ---------------------------> ~~~~~~~
    // Property 'toFixed' does not exist on type 'string | number | boolean'.
]

为什么编译器没有意识到 x 键与 number 值一起出现?为什么 val 键入为 string | number | boolean


问题是 KeyValFunc<T, keyof T> 不是您想要的元素类型。让我们检查一下 Foo:

type Test = KeyValFunc<Foo, keyof Foo>;
/* type Test = KeyValFunc<Foo, keyof Foo> */;

哦,这不是很有用。让我们定义一个身份 mapped type 以便我们可以在 KeyValFunc 上使用它来查看每个 属性.

type Id<T> = { [K in keyof T]: T[K] };

type Test = Id<KeyValFunc<Foo, keyof Foo>>;
/* type Test = {
     key: keyof Foo;
     valFunc: (val: string | number | boolean) => void;
} */

这就是问题所在。通过对K使用keyof T,每个数组元素可以有Fookeyof Foo)的任意属性键,而valFunc的参数可以是 Foo (Foo[keyof Foo]) 的任何 属性 值。这不是我们想要的。


而不是像K那样在KeyValFunc<T, K>中插入keyof T,我们真正想做的是分发KeyValFunc<T, K>K 中跨越 unions。也就是说,对于 keyof T 联合中的每个 K,我们要评估 KeyValFunc<T, K>,然后将它们组合在一个新的联合中。

这是一种写法:

type SomeKeyValueFunc<T> = { [K in keyof T]-?: KeyValFunc<T, K> }[keyof T]

这是一个映射类型,它的所有键立即 indexed into,以生成我们想要的联合 属性。让我们验证它是否按照我们想要的 Foo:

type Test = SomeKeyValueFunc<Foo>;
//type Test = KeyValFunc<Foo, "x"> | KeyValFunc<Foo, "y"> | KeyValFunc<Foo, "z">

是的,这样更好。现在 Test 本身是三种类型的联合,每种类型都是 KeyValFunc 对应 Foo 的特定键。所以对于 KeyValFuncArray<T>,我们想使用 SomeKeyValueFunc<T> 而不是 KeyValueFunc<T, keyof T>:

type KeyValFuncArray<T> = SomeKeyValueFunc<T>[];

然后事情突然按预期运行:

const arr: KeyValFuncArray<Foo> = [
    { key: "x", valFunc: val => val.toFixed() } // okay
]

Playground link to code