如何从对象元组创建对象类型?

vde*_*nne 6 tuples typescript mapped-types

从这个数据结构可以看出:

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
]
Run Code Online (Sandbox Code Playgroud)

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

type Person = {
  name: string;
  age: number;
  sex: 'm'|'f';
  username: string;
}
Run Code Online (Sandbox Code Playgroud)

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

Obl*_*sys 7

映射类型确实是正确的选择。为了确保没有类型被扩大,您必须as const添加properties

const properties = [
  { name: 'name', type: '' },
  { name: 'age', type: 0 },
  { name: 'sex', type: ['m', 'f'] as const },
  { name: 'username', type: '' }
] as const
Run Code Online (Sandbox Code Playgroud)

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

type FromProperties<P extends readonly unknown[]> = {
  [K in IndexKeys<P> as Name<P[K]>]: Type<P[K]>
} 
Run Code Online (Sandbox Code Playgroud)

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

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

type IndexKeys<A extends readonly unknown[]> = Exclude<keyof A, keyof []>
Run Code Online (Sandbox Code Playgroud)

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

type Name<O> = O extends { name: infer N } ? N extends string ? N : never : never
Run Code Online (Sandbox Code Playgroud)

要确定每个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
Run Code Online (Sandbox Code Playgroud)

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

根据这些定义,Properties可以构造

type Person = FromProperties<typeof properties>
// Inferred type:
// type Person = {
//     name: string;
//     age: number;
//     sex: "m" | "f";
//     username: string;
// }
Run Code Online (Sandbox Code Playgroud)

TypeScript 游乐场


Ole*_*ter 1

这其实并不难。您所需要的只是一个映射类型,其中键重新映射到元组成员的属性类型(从 4.1 开始name可以通过键重新映射很好地完成),并将值映射到相应成员的属性。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;
Run Code Online (Sandbox Code Playgroud)

接下来,让我们通过定义能够对元组成员的任意属性进行操作的泛型类型来使类型可重用。首先,我们需要提取索引以便能够将成员一对一映射到属性。这是通过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] 
};
Run Code Online (Sandbox Code Playgroud)

VPVV确保实用程序类型可以与任何同质对象元组一起使用,并且& string仅保留字符串兼容的属性。这已经给我们带来了一个相当不错的结果:

type PersonTest = TupleToProps<typeof properties, "name", "type">;
/**
 * type PersonTest = {
 *  name: "";
 *  age: 0;
 *  sex: readonly ["m", "f"];
 *  username: "";
 * }
 */
Run Code Online (Sandbox Code Playgroud)

之后,这只是一个表面上的更改问题:从类似元组的类型中提取值并扩大文字类型(由此问答提供,并进行了小升级以排除不应扩大的键):

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

操场