使用属性和泛型类型获取“keyof”中项目的类型

Lwy*_*yrn 2 typescript generic-type-parameters

我想定义一个具有泛型类型的接口,该接口必须接受一个对象,其键为“根字段名称”,值作为对象数组,该对象数组定义一些子字段,其键为子字段的名称, type 作为字段值的类型。像这样的东西:

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

const obj:Inputs = {emails: [ {email: "...", active: true, i: 100} ]}
Run Code Online (Sandbox Code Playgroud)

接收此泛型类型的接口具有一个“name”属性,该属性将接收子字段的(keyof)名称(例如 active ),以及一个带有参数的函数,该参数必须接收子字段的类型在 name 属性中定义。

像这样的东西

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

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

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

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

我不知道我是否能够很好地解释自己,但是我准备了一个沙箱来做得更好

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>
  );
}
Run Code Online (Sandbox Code Playgroud)

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

谢谢!

jca*_*alz 7

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

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

那么对于这个接口

interface Foo {
    x: number,
    y: string,
    z: boolean
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

type KeyValFuncArray<T> = KeyValFunc<T, keyof T>[];
Run Code Online (Sandbox Code Playgroud)

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

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

为什么编译器没有意识到x键与值相关number?为什么val输入为string | number | boolean


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

type Test = KeyValFunc<Foo, keyof Foo>;
/* type Test = KeyValFunc<Foo, keyof Foo> */;
Run Code Online (Sandbox Code Playgroud)

哦,这信息量不大。让我们定义一个身份映射类型,以便我们可以使用它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;
} */
Run Code Online (Sandbox Code Playgroud)

这就是问题所在。通过使用keyof Tfor ,每个数组元素可以具有( )K的任意属性键,并且参数 to可以是( )的任意属性值。那不是我们想要的。Fookeyof FoovalFuncFooFoo[keyof Foo]


我们真正想做的是在 中的联合中分发,而不是keyof TKin那样插入。也就是说,对于并集中的每个,我们要评估,然后将它们组合在一起形成一个新的并集。KeyValFunc<T, K> KeyValFunc<T, K>KKkeyof TKeyValFunc<T, K>

这是一种写法:

type SomeKeyValueFunc<T> = { [K in keyof T]-?: KeyValFunc<T, K> }[keyof T]
Run Code Online (Sandbox Code Playgroud)

这是一个映射类型,它会立即使用其所有键进行索引,以生成我们想要的联合属性。让我们验证一下它是否符合我们的要求Foo

type Test = SomeKeyValueFunc<Foo>;
//type Test = KeyValFunc<Foo, "x"> | KeyValFunc<Foo, "y"> | KeyValFunc<Foo, "z">
Run Code Online (Sandbox Code Playgroud)

是的,这样更好。NowTest本身是三种类型的联合,每种类型都KeyValFunc针对 的特定键Foo。因此对于KeyValFuncArray<T>,我们想使用SomeKeyValueFunc<T>而不是KeyValueFunc<T, keyof T>

type KeyValFuncArray<T> = SomeKeyValueFunc<T>[];
Run Code Online (Sandbox Code Playgroud)

突然事情就按预期进行了:

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

Playground 代码链接