【问题标题】:Typescript: Can you create a type based on a hierarchy?Typescript:你能根据层次结构创建类型吗?
【发布时间】:2021-05-10 21:53:27
【问题描述】:

可能有更好的方法来命名这个问题。如果你想到一个更好的名字,请告诉我,我会更改标题。我的问题有点复杂,但我有a code sample that demonstrates what I'm looking for well:

type Category = "FooAndBar" | "JustBaz" | "All"

export const widgets = [
  {
    name: 'Foo',
    categories: ["FooAndBar", "All"]
  },
  {
    name: 'Bar',
    categories: ["FooAndBar", "All"]
  },
  {
    name: 'Baz',
    categories: ["JustBaz", "All"]
  },
] as const;

export type WidgetName = typeof widgets[number]['name'];

export interface Widget {
  name: WidgetName;
  categories: Category[]
}

export interface WidgetPrices {
  location: string;
  // some field here that requires you to cover every type of widget,
  // whether it be from categories or specific names
}

// Here's how I would want WidgetPrices to work:

const valid = {
  location: "qazlandia",
  Foo: 1,
  Bar: 2,
  Baz: 3,
}

const valid2 = {
  location: "qazlandia",
  FooAndBar: 3,
  Baz: 3,
}

const valid3 = {
  location: "qazlandia",
  All: 10,
}

const valid4 = {
  location: "qazlandia",
  FooAndBar: 3,
  Bar: 5, // in code, I would probably give a specific name precedence over a category, but Foo would fallback to its category
  Baz: 3,
}

const valid5 = {
  location: "qazlandia",
  FooAndBar: 3,
  All: 5, // in code, I would probably give a specific category precedence over a broader category
}

const valid6 = {
  location: "qazlandia",
  FooAndBar: 3,
  JustBaz: 5,
}

const error = {
  location: "qazlandia",
  Foo: 1,
  // error: missing bar
  Baz: 3,
}

const error2 = {
  location: "qazlandia",
  FooAndBar: 3,
  // error, missing baz
}

问题是,可以用 typescript 的类型系统来定义这样的东西吗? WidgetPrices' 的声明是什么样的?

我在某些方面对 WidgetPrices' 声明很灵活,例如,我更喜欢实现看起来像我呈现的方式,但我可以单独使用 NamesCategory 属性,如果这是解决这个问题的唯一方法。对我来说最重要的是当我错过覆盖范围时编译器会出错。

【问题讨论】:

  • 嗨,您是否有任意数量的字段要添加到小部件价格中?在这种情况下,我会添加一个通用的可选参数,例如 [_: string] :?任何。如果您可以添加有限数量的价格类型,我会从小部件价格扩展它们。最后,您可以考虑使用类型为 T 的 params 属性的类型化 wodgetPrices
  • 每次我向widgets 添加一个新元素时,WidgetPrices 都会得到一个新字段。我不知道这是不是你要问的,@CarlosMoura
  • 这是我为之而生的问题。

标签: typescript


【解决方案1】:

我有一些适用于您的示例的东西,其中WidgetPricesunion 值属性的键的有效选择的特定union。这个联合的成员数量随着 Widget 名称或类别的数量线性增长而呈指数增长。这意味着如果您的层次结构比示例中显示的大一些,您可能会遇到编译器性能问题或其他限制,如果是这样,您可能需要从特定类型退回到 generic 类型。现在我只担心联合版本,如果用例需要,可能稍后再回来处理通用版本:

type Widgets = typeof widgets[number]
type Price<K extends PropertyKey> = K extends PropertyKey ? { [P in K]: number } : never;
type _WidgetPrices<W extends Widgets = Widgets> =
  (W extends any ? (x: Price<W["name"] | W["categories"][number]>) => void : never) extends
  ((x: infer I) => void) ? I : never
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never
type WidgetPrices = Id<{ location: string } & _WidgetPrices>;

在我开始解释之前,让我们看看WidgetPrices 是否正确。如果您使用 IntelliSense 进行检查,则会显示:

/* type WidgetPrices = {
    location: string;
    All: number;
} | {
    location: string;
    Foo: number;
    FooAndBar: number;
    All: number;
} | {
    location: string;
    Foo: number;
    FooAndBar: number;
    Baz: number;
} | {
    location: string;
    Foo: number;
    FooAndBar: number;
    JustBaz: number;
} | ... 21 more ... | {
    ...;
} */

显示的每个工会成员都对应一个有效的价格选择。它们都有一个location 属性,它们的其他字段涵盖了全套小部件。第一个只有All,就够了。其他人有不同的选择。您可以验证您的所有 valid… 值是否有效:

const valid: WidgetPrices = {
  location: "qazlandia",  Foo: 1,  Bar: 2,  Baz: 3,
}

const valid2: WidgetPrices = {
  location: "qazlandia",  FooAndBar: 3,  Baz: 3,
}

const valid3: WidgetPrices = {
  location: "qazlandia",  All: 10,
}

const valid4: WidgetPrices = {
  location: "qazlandia", FooAndBar: 3, Bar: 5, Baz: 3,
}

const valid5: WidgetPrices = {
  location: "qazlandia", FooAndBar: 3, All: 5,
}

const valid6: WidgetPrices = {
  location: "qazlandia",  FooAndBar: 3,  JustBaz: 5,
}

error… 失败时:

const error: WidgetPrices = { // error!
  //  ~~~~~
  // Property 'Bar' is missing in type '{ location: string; Foo: number; Baz: number; }' 
  // but required in type '{ location: string; Foo: number; Bar: number; Baz: number; }'
  location: "qazlandia",
  Foo: 1,
  Baz: 3,
}

const error2: WidgetPrices = { // error!
  //  ~~~~~~
  // Type '{ location: string; FooAndBar: number; }' is missing the following 
  // properties from type '{ location: string; All: number; FooAndBar: number; JustBaz: number; }': 
  // All, JustBaz
  location: "qazlandia",
  FooAndBar: 3,
}

好的,现在是解释的草图:

  • Widgets 类型只是 widgets 数组中元素类型的联合。
  • 对于 K 联合中的每个键,Price&lt;K&gt; 将键的联合 K 转换为具有 number-valued 属性的单键对象的联合。所以Price&lt;"a" | "b"&gt; 变成了{a: number} | {b: number}
  • 繁重的工作在_WidgetPrices&lt;W&gt;。我们在该联合上给它一个小部件 Wdistribute 的联合。对于W 中的每个元素,我们计算Price&lt;W["name"] | W["categories"][number]&gt;。因此,例如,Foo 元素变为Price&lt;"Foo" | "FooAndBar" | "All"&gt;{Foo: number} | {FooAndBar: number} | {All: number}。这些是仅涵盖Foo 小部件的所有对象类型;让我们将其命名为CoverageForFoo。然后我要做的是通过technique for getting the compiler to calculate arbitrary-size intersections 获取unions-of-coverage-objects 的intersection。如果WWidgets,那么这本质上就是我们正在寻找的东西。也就是说,你想要CoverageForFoo &amp; CoverageForBar &amp; CoverageForBaz。这样的对象类型肯定会覆盖Foo Bar Baz
  • 一个大的旧联合交集是uuuugly,所以我使用另一个technique to eliminate some of those intersectionsId&lt;T&gt; 类型或多或少只是产生与T 等效的对象类型,但类似Id&lt;{a: number} &amp; {b: string}&gt; 的东西会变成{a: number, b: string}
  • 最后,WidgetPricesId&lt;{location: string} &amp; _WidgetPrices&lt;Widgets&gt;&gt;:一个包含 string-valued location 属性和 number-valued 属性的事物的模糊不丑的结合,涵盖所有小部件。

所以,万岁!正如我所说,如果你在其中添加太多东西,这可能会爆炸。转换此泛型的方法将涉及一个辅助函数,该函数检查某个值是否为某个合适的泛型类型参数WWidgetPrices&lt;W&gt;,但是当您想谈论小部件价格时,您必须随身携带泛型。而且由于我不知道用例是否需要它,而且我已经花了很多时间在这上面,所以我会在这里停下来。

Playground link to code

【讨论】:

  • 还有WidgetName吗?它的类型会改变吗?
  • 我没有碰WidgetName,但你没有理由不能使用它;相当于Widgets["name"]
猜你喜欢
  • 2021-12-06
  • 2021-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-13
  • 1970-01-01
  • 2023-03-29
相关资源
最近更新 更多