typescript - 从对象类型创建常量数组类型

标签 typescript

我有一个类似于此的对象类型:

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

我也有一个类似这样的只读数组:

// Type: readonly ["countryCode", "currency", "otherFields"]
const allowedFields = ["countryCode", "currency", "otherFields"] as const;

我希望能够根据 Fields 对象类型为此数组声明指定一个接口(interface),以便对其进行的任何更改也需要更改数组。像这样:

// How to create 'SomeType'?
const allowedFields: SomeType = ["countryCode"] as const; // Should throw error because there are missing fields

const allowedFields: SomeType = ["extraField"] as const; // Should throw error because "extraField" is not in the object type 'Fields'

最佳答案

type Fields = {
  countryCode: string;
  currency: string;
  otherFields: string;
};

// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
  [S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];


type AllowedFields = TupleUnion<keyof Fields>;


const allowedFields: AllowedFields = ["countryCode", "currency", "otherFields"];


// How to create 'SomeType'?
const foo: AllowedFields  = ["countryCode"]; // Should throw error because there are missing fields

const bar: AllowedFields  = ["extraField"]; // Should throw error because "extraField" is not in the object type 'Fields'

您需要创建所有允许的 Prop 的排列。为什么排列?因为字典的键是无序的。

Playground

解释

让我们摆脱递归调用和条件类型:

{
  type TupleUnion<U extends string, R extends any[] = []> = {
    [S in U]: [...R, S]
  }

  type AllowedFields = TupleUnion<keyof Fields>;
  type AllowedFields = {
    countryCode: ["countryCode"];
    currency: ["currency"];
    otherFields: ["otherFields"];
  }
}

我们创建了一个对象,其中每个值都是一个包含 key 的元组. 为了完成任务,每个值应该包含不同顺序的每个键。 像那样:

  type AllowedFields = {
    countryCode: ["countryCode", 'currency', 'otherFields'];
    currency: ["currency", 'countryCode', 'otherFields'];
    otherFields: ["otherFields", 'countryCode', 'currency'];
  }

因此,为了添加另外两个 Prop ,我们需要调用 TupleUnion递归地,但没有元组中已经存在的元素。这意味着,我们的第二个调用应该这样做:


  type AllowedFields = {
    countryCode: ["countryCode", Exclude<Fields, 'countryCode'>];
    currency: ["currency", Exclude<Fields, 'currency'>];
    otherFields: ["otherFields", Exclude<Fields, 'otherFields'>];
  }

为了实现,我们需要这样做:TupleUnion<Exclude<U, S>, [...R, S]>; .如果我这样写可能会更易读:

type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

但是接下来我们将生成深度嵌套的数据结构:

  type AllowedFields = TupleUnion<keyof Fields>['countryCode']['currency']['otherFields']

我们不应该调用 TupleUnion递归如果 Exclude<U, S> ,或者换句话说 Exclude<FieldKeys, Key>返回 never .我们需要检查是否 Key是最后一个属性。为此,我们可以检查是否 Exclude<U, S> extends never .如果是 never - 没有更多的 key ,我们可以返回 [...R,S] .

我希望这段代码:

{
  type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }

  type AllowedFields = TupleUnion<keyof Fields>

}

更清晰。但是,我们仍然有一个带有值而不是元组的对象。对象中的每个值都是所需类型的元组。为了获得所有值的并集,我们只需要使用方括号表示法和所有键的并集。像那样:type A = {age:1,name:2}['age'|'name'] // 1|2 .

最终代码:

 type TupleUnion<FieldKeys extends string, Result extends any[] = []> = {
    [Key in FieldKeys]: Exclude<FieldKeys, Key> extends never ? [...Result, Key] : TupleUnion<Exclude<FieldKeys, Key>, [...Result, Key]>;
  }[FieldKeys] // added suqare bracket notation with union of all keys

关于typescript - 从对象类型创建常量数组类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69676439/

相关文章:

javascript - 如何在 Angular 8 上选择下拉菜单中获取颜色?

node.js - 如何使用 Typescript 读取多个 parquet 文件?

Angular 2 使用 Http Observable 和 Pipe

angular - 直接 URL 路由工作正常,但不适用于域名

javascript - ReactiveX 中数组实体和子项的调用方法

动态对象属性的 Typescript 动态联合类型

javascript - 在有保证的 typescript 方法内有序执行事件

angular - Angular 2/4 上的全屏请求

typescript - 具有复杂属性类型的对象常量的类型定义

typescript :对象可能是 'undefined' 但仅适用于 '<' 和 '>' 运算符