包含用作接口密钥的 keyof 的模板文字

Template literal containing keyof used as an interface key

给定一个简单的界面:

interface Schedule {
  interval: "month" | "day";
  price: number;
}

我可以构造一个模板文字类型(使用ts4.4):

type PrefixedScheduleKey = `schedule__${keyof Schedule}`; // "schedule__interval" | "schedule__price"

假设有几个这样的接口,我想合并(基本上扁平化)它们的前缀、模板文字键以结束:

interface Metadata {
  foo: string;
  bar: string;
  // computed 
  schedule__interval: "month" | "day";
  schedule__price: number;
  ...
}

我应该如何声明这样的接口键?

interface Metadata {
  [key: PrefixedScheduleKey]: any
}

// throws 
// An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.(1337)

interface Metadata {
  [key in `schedule__${keyof Schedule}`]: any 
}

// leads to
// A computed property name must be of type 'string', 'number', 'symbol', or 'any'.(2464)
// Member '[key in `schedule__${keyof' implicitly has an 'any' type.(7008)

这是 playground

顺便说一句,suggested on github 使用 type 而不是 interface 但这行不通,因为我的 Metadata 接口已经扩展了另一个接口,但我试图保持示例简单。

这不可能直接在界面上执行,如此处记录:

So far in TypeScript (as of version 2.7.2), union type signatures in interfaces are not allowed and instead mapped types should be used (as you correctly have).

Docs.

但是,您可以使用任意接口和映射类型之间的交集类型来实现这一点,as in the original GitHub issue you mentioned ("Moves other members to a separate object type that gets combined with an intersection type"). You can also use the as keyword 以有效地内联您的 PrefixedScheduleKey 模板文字类型,同时保留您的 Key足够长以引用 Schedule[Key]。没有理由不能对其他前缀类型执行相同的操作,无论是在相同的映射元数据类型中还是在您稍后与之相交的相邻元数据类型中。

作为参考,key 在映射类型中用作类型变量时大写为 Key,如 mapped types handbook page.

interface BareMetadata /* extends ArbitraryType */ {
  foo: string;
  bar: string;
}

type ScheduleMetadata = {
  [Key in keyof Schedule as `schedule__${Key}`]: Schedule[Key];
}

type MergedMetadata = BareMetadata & ScheduleMetadata;

const example: MergedMetadata = {
  foo: "one",
  bar: "two",
  schedule__interval: "month",
  schedule__price: 9.99
};

Playground Link

您甚至可以 have the interface extend an object-like type,这可能会帮助您避免显式交集:

interface MergedMetadata extends BareMetadata, ScheduleMetadata {}
// or
interface Metadata extends ScheduleMetadata {
  foo: string;
  bar: string;
}

Playground Link