【问题标题】:recursively get property names of an object/record递归获取对象/记录的属性名称
【发布时间】:2021-02-26 10:54:34
【问题描述】:

给定一个 {...} 形式的对象 - 不是原语或数组 - 我想生成一个类型,它是该类型中的文字属性名称。我试过用映射类型来做这件事,但做错了。不知道我做错了什么。我可以深入一层(这实际上很容易使用 keyof 运算符)但无法弄清楚如何递归地进行。我知道我很接近,因为在下面的示例中,如果 email 属性不是可选的,它可以工作。所以不知何故,它被可选属性绊倒了。

type PropertyNames<
  T extends Record<string, unknown>,
  MODE extends 'deep' | 'shallow'
> = {
  [KEY in keyof T & (string | number )]:
    | KEY
    | (MODE extends 'deep'
        ? T[KEY] extends Record<string, unknown>
          ? PropertyNames<T[KEY], 'deep'>
          : never
        : never);
}[keyof T & (string | number)];

type Email = {
  address: string;
  verified: boolean;
};

type UserToken = {
  uid: string;
  name?: string;
  email?: Email;
};

// Expected: uid | name | email | address | verified
// Actual  : uid | name | email
type DeepNames = PropertyNames<UserToken, 'deep'>;

// Expected and Actual : uid | name | email
type ShallowNames = PropertyNames<UserToken, 'shallow'>;

【问题讨论】:

    标签: typescript


    【解决方案1】:

    我认为您的主要问题是T[KEY] extends Record&lt;string, unknown&gt; ? ... 不是distributive 超过T[KEY] 中可能的联合,因此当KEY 是可选属性时,T[KEY] 将包含undefined,并且由于@987654329 @ 不是真的,整个事情都失败了,你得到never。最好使用分布在联合上的公式。

    我倾向于做这样的事情:

    type PropertyNames<T, M extends 'deep' | 'shallow'> =
        T extends object ?
        { [K in keyof T]-?: K |
            (M extends 'deep' ? PropertyNames<T[K], "deep"> : never)
        }[keyof T] : never
    

    或者,如果您在其中混合了数组类型,则:

    type PropertyNames<T, M extends 'deep' | 'shallow'> =
        T extends object ?
        { [K in keyof T]-?: K |
            (M extends 'deep' ? PropertyNames<T[K], "deep"> : never)
        }[Extract<keyof T, T extends readonly any[] ? number : unknown>] : never
    

    这会为您的示例产生:

    type DeepNames = PropertyNames<UserToken, 'deep'>;
    // type DeepNames = "uid" | "name" | "email" | "address" | "verified"
    

    根据需要。


    我所做的更改:

    • 我已经放弃了Record&lt;string, unknown&gt;,因为object 的问题较少,因为没有索引签名的interface 类型往往不能分配给Record&lt;string, unknown&gt;

      interface Oops {
        x: string;
      }
      
      type Nope = Oops extends Record<string, unknown> ? "yep" : "nope";
      // type Nope = "nope"
      
    • 我已经放弃了 T 作为对象类型的限制;这使得对PropertyNames&lt;T[K]&gt; 的递归评估更容易;如果T 不是对象,则返回never。这也会自动将操作分配给T 中的联合。虽然T[K] extends ... 不会分发,但T extends ... 会分发,因为T 是一个“裸”类型参数。

    • 我已经使用the -? modifier 使映射类型将所有属性转换为所需属性;这将消除可能出现在最终输出中的任何奇怪的undefineds。

    • 我已将keyof T &amp; (string | number) 替换为keyof T,因为除非我们试图禁止symbol 键,否则我认为这并不重要。

    • Extract&lt;keyof T, T extends readonly any[] ? number : unknown&gt; 的东西只是处理数组;您可能不想在输出中看到数组方法的所有名称,例如 "push""pop"(如果这样做,则可以改用 keyof T)。所以对于数组,只需查看数字索引处的属性即可。

    Playground link to code

    【讨论】:

    • 完美的超级有用的答案。我想出了自己的版本 - 用 Required 包装每个 T 但你的更短,我非常感谢您的解释。
    【解决方案2】:

    此解决方案与您的实现非常相似,但需要将任何对象类型的所有键传递给PropertyNames

    type Email = {
      address: string;
      verified: boolean;
    };
    
    type UserToken = {
      uid: string;
      name?: string;
      email?: Email;
    };
    
    type PropertyNames<T, MODE extends "deep" | "shallow"> =
      | keyof T
      | {
          [K in keyof T]-?: Required<T>[K] extends Record<string, any>
            ? MODE extends "shallow"
              ? never
              : PropertyNames<Required<T>[K], MODE>
            : never;
        }[keyof T];
    
    // uid | name | email | address | verified
    type DeepNames = PropertyNames<UserToken, "deep">;
    
    // uid | name | email
    type ShallowNames = PropertyNames<UserToken, "shallow">;
    
    

    【讨论】:

    • 谢谢。我看到这如何解决我的解决方案的问题 - 包装在Required中。
    猜你喜欢
    • 2011-05-12
    • 2013-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-11
    • 2018-01-19
    相关资源
    最近更新 更多