【问题标题】:How to create an object type from tuple of objects?如何从对象元组创建对象类型?
【发布时间】:2021-06-04 14:48:56
【问题描述】:

从此数据结构:

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
]

我正在尝试动态构建以下类型:

type Person = {
  name: string;
  age: number;
  sex: 'm'|'f';
  username: string;
}

我知道 TypeScript 可以通过映射其他类型来创建类型。但是这里的源对象是一个数组,所以看起来有点棘手。我该怎么做?

【问题讨论】:

  • 我认为您必须编写一个脚本,将 properties 数组转换为类型声明并将其放入 .d.ts 文件中。

标签: typescript tuples mapped-types


【解决方案1】:

这实际上并不难。您所需要的只是一个映射类型,其键重新映射到元组成员的 name 属性的类型(可以很好地使用 key remapping since 4.1 完成)和相应成员的 type 属性的值。

首先,您需要让编译器知道properties 数组实际上是一个带有as const 断言的元组:

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
] as const;

接下来,让我们通过定义一个能够对元组成员的任意属性进行操作的泛型类型来使该类型可重用。首先,我们需要提取索引以便能够映射 成员与属性一对一。这是通过Exclude<keyof T, keyof readonly any[]> 完成的(仅保留索引)。

我们可以使用生成的索引联合来进行映射:

type TupleToProps<T extends readonly any[], VP extends keyof T[number], VV extends keyof T[number]> = {
    [ P in Exclude<keyof T, keyof readonly any[]> as T[P][VP] & string ] : T[P][VV] 
};

VPVV 确保实用程序类型可以与任何同质的对象元组一起使用,&amp; string 只留下与字符串兼容的属性。这已经给我们带来了非常好的结果:

type PersonTest = TupleToProps<typeof properties, "name", "type">;
/**
 * type PersonTest = {
 *  name: "";
 *  age: 0;
 *  sex: readonly ["m", "f"];
 *  username: "";
 * }
 */

在那之后,这只是表面上的改变:从类似元组的类型中提取值并扩大文字类型(由thisthis Q&A 提供,小幅升级以排除不应扩大的键) :

type ToPrimitive<T> =
  T extends string ? string
  : T extends number ? number
  : T extends boolean ? boolean
  : T;
// mapped types which will preserve keys with more wide value types
type Widen<O, E = never> = {
  [K in keyof O]: K extends E ? O[K] : ToPrimitive<O[K]>
}

type TupleToPropsTwo<T extends readonly any[], VP extends keyof T[number], VV extends keyof T[number], E extends T[number][VP]> = Widen<{
    [ P in Exclude<keyof T, keyof readonly any[]> as T[P][VP] & string ] : T[P][VV] extends readonly any[] ? T[P][VV][number] : T[P][VV]
}, E>;

type PersonTest2 = TupleToPropsTwo<typeof properties, "name", "type", "sex">;
/**
 * type PersonTest2 = {
 *  name: string;
 *  age: number;
 *  sex: "m" | "f";
 *  username: string;
 * }
 */

Playground

【讨论】:

    【解决方案2】:

    映射类型确实是要走的路。为确保没有类型得到扩展,您必须将 as const 添加到 properties

    const properties = [
      { name: 'name', type: '' },
      { name: 'age', type: 0 },
      { name: 'sex', type: ['m', 'f'] as const },
      { name: 'username', type: '' }
    ] as const
    

    然后您可以使用映射类型,在其中将键从数组索引重新映射到属性名称,并将值映射到类型。映射类型本身如下所示:

    type FromProperties<P extends readonly unknown[]> = {
      [K in IndexKeys<P> as Name<P[K]>]: Type<P[K]>
    } 
    

    它映射作为实际索引的键('0''1' 等)而不是数组属性,将键重新映射到属性 name,并将 Type&lt;P[K]&gt; 作为值。

    要获得正确的索引键,您可以从数组中排除 [] 上的属性,这样只会留下索引:

    type IndexKeys<A extends readonly unknown[]> = Exclude<keyof A, keyof []>
    

    要获取名称,可以使用简单的条件类型:

    type Name<O> = O extends { name: infer N } ? N extends string ? N : never : never
    

    要确定每个type 值的实际类型,可以使用稍微复杂一点的条件类型:

    type Type<O> = 
      O extends { type: infer T }
      ?   T extends number ? number 
        : T extends string ? string 
        : T extends readonly unknown[] ? T[number]
        : never // `name` property is not a number, string or array
      : never // object has no `type` property
    

    extends 子句是为了防止推断类型太窄(例如'' 而不是string)并推断数组的联合类型。如果需要,您可以添加更多类型。

    通过这些定义,Properties 可以构造成

    type Person = FromProperties<typeof properties>
    // Inferred type:
    // type Person = {
    //     name: string;
    //     age: number;
    //     sex: "m" | "f";
    //     username: string;
    // }
    

    TypeScript playground

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-16
      • 1970-01-01
      相关资源
      最近更新 更多