打字稿模板文字作为界面键

时间:2021-01-25 04:11:19

标签: typescript

假设我想在打字稿中创建一个包含多个项目的对象,如下所示:

const obj: Items = {
  item1: 'foo',
  item2: 'bar',
  item3: 'baz',
}

我应该如何声明我的 Items 类型以使其与任意数量的项目兼容?我使用 Typescript 4.1 中的模板文字尝试了以下操作,但似乎不起作用:

interface Items {
  [P: `array${number}`]: any;
}

是否可以声明这样的类型?

6 个答案:

答案 0 :(得分:8)

TS4.4+ 更新

TypeScript 4.4 将支持包含模式模板文字的索引签名,如在 microsoft/TypeScript#44512 中实现的那样。然后您就可以将 Items 声明为特定类型,如下所示:

interface Items {
  [key: `item${number}`]: any;
}

您可以验证它是否按预期工作:

const obj: Items = {
  item1: 'foo',
  item2: 'bar',
  item2021: 'baz',
  item3: 'qux',
};

const objBad: Items = {
  item1: 'foo',
  item2: 'bar',
  itemMMXXI: 'baz', // error!
  //  ~~~~~~~~~ <--
  //  Object literal may only specify known properties,
  //  and 'itemMMXXI' does not exist in type 'Items'
  item3: 'qux'
};

Playground link to code


对 TS4.1-4.3 的回答

从 TypeScript 4.1 开始,目前不允许将 `item${number}` 形式的模式模板文字(在 microsoft/TypeScript#40598 中实现)作为键类型。

目前没有与您想要的 Items 类型相对应的特定类型。相反,您可以将其表示为类型上的约束,并编写一个仅接受遵守约束的输入的辅助函数 asItems()

const asItems = <K extends PropertyKey>(
    obj: { [P in K]: P extends `item${number}` ? any : never }
) => obj;

将检查传入的 obj 的每个键是否可分配给 `item${number}`。如果是,则属性类型为any,否则,属性类型为never。这往往会导致任何违规属性出现错误:

const obj = asItems({
    item1: 'foo',
    item2: 'bar',
    item2021: 'baz',
    item3: 'qux',
}); // okay

const objBad = asItems({
    item1: 'foo',
    item2: 'bar',
    itemMMXXI: 'baz', // error!
//  ~~~~~~~~~ <-- Type 'string' is not assignable to type 'never'
    item3: 'qux'
});

Playground link to code

答案 1 :(得分:1)

我最终采用了一种方法,首先构造一个固定长度的元组(因为它的键不能无限长),然后在过滤掉非数字键的同时遍历它们,并用它来构造 Items 类型。然而,如上所述,一个警告是数量不能超过限制(恰好是 44),但对于我的用例来说已经足够了,所以我很满意。

// https://github.com/Microsoft/TypeScript/issues/26223#issuecomment-513116547
type PushFront<TailT extends any[], FrontT> = ((...args: [FrontT, ...TailT]) => any) extends (...tuple: infer TupleT) => any ? TupleT : never;
type Tuple<ElementT, LengthT extends number, OutputT extends any[] = []> = {
    0: OutputT;
    1: Tuple<ElementT, LengthT, PushFront<OutputT, ElementT>>;
}[OutputT['length'] extends LengthT ? 0 : 1];
const N = 44;
// N larger than 44 seems to exceed recursion limit
// const N = 45;
type NameN<Name extends string, T> = {
    [P in keyof Tuple<any, typeof N> as P extends `${number}` ? `${Name}${P}` : never]: T;
};

type Items = NameN<'item', any>

Playground Link

答案 2 :(得分:1)

如果您可以接受的属性数量有一个合理的最大值,您可能只想让编译器为您计算这些属性。例如,以下将通过 item1 生成 item249

type Foo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // 10
type Bar = [...Foo, ...Foo, ...Foo, ...Foo, ...Foo]; // 50
type Baz = [...Bar, ...Bar, ...Bar, ...Bar, ...Bar]; // 250
type ItemNames = `item${Exclude<keyof Baz, '0' | keyof any[]>}`
// item1 through item249
type ItemProps = { [K in ItemNames]?: any };
interface Items extends ItemProps {
}

const obj: Items = {
    item1: 'foo',
    item2: 'bar',
    item3: 'baz',
} // okay


const objBad: Items = {
    item1: 'foo',
    item2: 'bar',
    itemMMXXI: 'baz', // error!
    //~~~~~~~~~~~~~ <-- Object literal may only specify known properties, and 'itemMMXXI'
    // does not exist in type Items
};

在编译器开始抱怨之前,您可以将其扩展到小于 10,000 的最大值:

type Foo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // 10
type Bar = [...Foo, ...Foo, ...Foo, ...Foo, ...Foo]; // 50
type Baz = [...Bar, ...Bar, ...Bar, ...Bar, ...Bar]; // 250
type Qux = [...Baz, ...Baz, ...Baz, ...Baz, ...Baz]; // 1250
type Quux = [...Qux, ...Qux, ...Qux, ...Qux, ...Qux]; // 6250
// type Suux = [...Quux, ...Quux]; // error: 
//   Type produces a tuple type that is too large to represent.

type ItemNames = `item${Exclude<keyof Quux, '0' | keyof any[]>}`
// item1 through item6249

Playground link to code

答案 3 :(得分:0)

https://basarat.gitbook.io/typescript/type-system/index-signatures

你很接近

interface MyInterface {
   [key: string]: string;
}

const x: MyInterface = {
   item1: "string"
}

答案 4 :(得分:0)

不可能完全这样声明类型。 Danie A 的答案是您真正能得到的最接近的答案,但是您将接受所有字符串。如果您知道您需要的数字数量有上限并且您愿意将它们全部输入,您可以执行以下操作:

map!(a->a>5 ? 5 : a, x, x)

我相信你已经知道你可以做到这一点。

答案 5 :(得分:0)

这是我的解决方案:

type Item<n extends number> = `item${n}`
type Items<n extends number[]> = {
    [key in Item<n[number]>]: any
}

const goodObj: Items<[1, 2, 2021, 3]> = {
    item1: 'foo',
    item2: 'bar',
    item2021: 'baz',
    item3: 'qux', 
}; // okay

const badObj: Items<[1, 2, 2021, 3]> = {
    item1: 'foo',
    item2: 'bar',
    itemMMXXI: 'baz', // error!
    //  ~~~~~~~~~ <-- Type '{ item1: string; item2: string; itemMMXXI: string; item3: string; }' is not assignable to type 'Items<[1, 2, 2021, 3]>'.
    //                Object literal may only specify known properties, and 'itemMMXXI' does not exist in type 'Items<[1, 2, 2021, 3]>'
    item3: 'qux'
};