【问题标题】:Typescript - Determine Child type based on property given from keyofTypescript - 根据 keyof 给出的属性确定子类型
【发布时间】:2023-04-07 05:43:01
【问题描述】:

我不确定这是否可行,但我希望能够采用一个界面并为该界面提供额外的选项。

interface IRole {
  id: number;
  name: string;
}

interface IAddress {
  line1: string;
  line2: string;
  city: string;
  state: string;
  zip: string;
}

interface IUser {
  email: string;
  password: string;

  role?: IRole;
  addresses: IAddress[];
}

const options: FieldOptions<IUser> = {
  name: 'role',
  description: 'This is a role',
  children: [
    {
      name: 'name', // The child element declaration should be the type that is specified by the name
                    // In this case it should be FieldOptions<IRole>[]
                    // But right now it is FieldOptions<IRole | IAddress[] | string>[]
                    // I just need a way to narrow down to what it should be
      description: 'This is the name on the role'
    }
  ]
}

现在我有我的 FieldOptions 接口,我知道这是错误的。

interface FieldOptions<T, K extends keyof T> {
  name: K;
  description?: string;
  children?: FieldOptions<T[K], keyof T[K]>[];
}

我一直在尝试找到一个示例,但是我发现的所有示例都与已知键有关,并将为此使用条件属性。我的有点不同,因为它建立在未知之上,直到用户输入类型。感谢您为我提供的任何帮助。

【问题讨论】:

  • 我正在努力。您需要使用mapped types。递归使它有点棘手。

标签: typescript typescript-typings typescript-generics


【解决方案1】:

我很确定你需要一个类型别名而不是一个接口,因为FieldOptions&lt;IUser&gt; 最终应该是一个union 结构,对应于name 字段的每个选项。并且接口不能是联合。

以下是递归条件映射类型,希望能表达您想要完成的任务:

type FieldOptions<T> = T extends object ? { [K in keyof T]-?: {
  name: K;
  description?: string;
  children?: FieldOptions<T[K]>[];
} }[keyof T] : never;

这个想法是,如果T 是一个对象类型,您将遍历其所有属性键K 并计算类似于您之前版本的东西,我将其称为OldFieldOptions&lt;T, K&gt;。这会产生类似{email: OldFieldOptions&lt;IUser, "email"&gt;, password: OldFieldOptions&lt;IUser, "password", ...} 的东西。然后我们通过使用keyof T 对其所有属性类型进行索引来look up。这会产生联合 OldFieldOptions&lt;IUser, "email"&gt; | OldFieldOptions&lt;IUser, "password"&gt; | ...

由于FieldOptions&lt;T&gt; 是递归的,这也应该适用于所有子属性。

如果T 不是对象,我们只返回never(所以children 将是never[],意味着它不能有任何条目)。如果T 是一个数组,这可能会做一些奇怪的事情,所以您可能需要对其进行调整。

让我们看看它是否有效:

const options: FieldOptions<IUser> = {
  name: 'role',
  description: 'This is a role',
  children: [
    {
      name: 'name',
      description: 'This is the name on the role',
      children: []
    }
  ]
}; // okay

const badOptions: FieldOptions<IUser> = {
  name: "role",
  children: [{
    name: "oops" // error!
  //~~~~ <-- bad name
  }]
}

看起来不错。

正如我所提到的,您可能需要调整上面的定义,以防您想以不同的方式处理原始属性或数组属性。现在它最适合普通对象类型。你必须考虑FieldOptions&lt;IAddresses[]&gt;,例如:它的name属性应该是什么,数字?

Playground link to code

【讨论】:

  • 这很有趣。知道为什么选择 name 会缩小此定义中的 children 类型,但当 OP 包含应该 force its type to that literal? Playground linkname: 'role' as const 断言时则不然
  • 这就像一个魅力现在让我一次将所有想法都包裹起来,还能够使用带有推断的条件来定义数组 typedef。
【解决方案2】:

@jcalz 打败了我,但我想出的却非常相似。

type FieldOptions<T> = {
  [K in keyof T]: {
    name: K;
    description: string;
    children?: Required<T>[K] extends object ? FieldOptions<Required<T>[K]> : never;
  }
}[keyof T][]

棘手的部分是必须使用Required&lt;T&gt;[K] 而不仅仅是T[K]。这是因为 IUser['role'] 之类的东西是 IRole | undefined,因为该属性是可选的,但我们希望获得 IRole 的选项,而不会被 undefined 弄乱。

type UserOptions = FieldOptions<IUser>

评估为:

type UserOptions = ({
    name: "email";
    description: string;
    children?: undefined;
} | {
    name: "password";
    description: string;
    children?: undefined;
} | {
    name: "role";
    description: string;
    children?: FieldOptions<IRole> | undefined;
} | {
    ...;
} | undefined)[]

我假设您的 options 应该是一个包含该对象的数组,因为否则它实际上没有任何意义。 passes typescript checks 与我的类型 FieldOptions&lt;IUser&gt;.

【讨论】:

    猜你喜欢
    • 2018-06-19
    • 2021-11-13
    • 2018-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-28
    • 2021-02-22
    相关资源
    最近更新 更多