模板文字键具有 'any' 类型,因为 'string' 类型的表达式不能用于索引类型

Template literal key has an 'any' type because expression of type 'string' can't be used to index type

关于如何断言特定键属于特定接口或类型,有许多类似的问题,例如 。我的问题是不同的,因为所讨论的密钥是由两部分组成的,而不是简单地使用 Object.keys.

之类的密钥

这似乎与 mapped types 有关,但我什至不知道这是否是 TypeScript 可以解决的问题。目前,我有许多接口充满了 属性 名称,这些名称是名称前缀(如 team)和数字部分的可预测组合。这些数字总是在某个范围内,例如 1-161-18。我已经添加了这些没有映射类型的接口,所以它们是非常大的接口,我希望以后使用映射类型。

我正在逐渐将文件从 JavaScript 转换为 TypeScript。大多数新转换的 TypeScript 代码仍然被错误地输入为 any 类型。它涉及先前范围的迭代,如下所示:

const name = record[`team${num}`]; // num is from 1-16

我知道 record['team${num}'] 不是 any 而是 string 类型。如果我只是简单地写出几十个 属性 的名字,使用点符号 a la record.team1; 那么就没有错误并且类型检查工作正常。简单断言 as stringas unknown as stringas (typeof record[keyof typeof record]) 都没有从有问题的代码中删除错误消息。我现在知道如何 suppress 使用类型断言输入错误,但请注意代码会导致一个错误,现在没有错误,而实际上它是运行时错误。

有没有一种惯用的方法可以使用 TypeScript 检查 类型?我不想出现差一错误,等等。我的一些代码可以重写以使用现有属性,而其他代码则根据使用名称和数字的组合索引的那些现有属性创建新属性。

你可以使用众所周知的Template Literal Types。只要知道团队名称和人数。可能的解决方案如下所示:

type TeamNumber = 1 | 2 
type TeamName = 'TeamA' | 'TeamB'
type TeamData = { trainer?: string }

// here is where the magic happens: 
// we compute all keys based on other union types inside a template string
type TeamRecord = {
  [K in `${TeamName}${TeamNumber}`]: TeamData;
};

// you get autocompletion for all properties
const teamRecord: TeamRecord = {
  TeamA1: {},
  TeamA2: {},
  TeamB1: {},
  TeamB2: {}
}

随意尝试一下 TypescriptPlayground


编辑:

如果您需要遍历 TeamNumber 的值,您还可以从数组创建 TeamNumber 类型。因此,不需要 const max 变量来防止迭代中出现虚假值。

const teamNumbers = [1, 2, 3, 4] as const;

type TeamNumber = typeof teamNumbers[number];
type Team = 'team';
type VerboseTeamRecord = Teams & {
  id: number;
  event: string;
  gender: 'M' | 'F' | 'B';
};
type Teams = {
  [K in `${Team}${TeamNumber}`]: string;
};

let verboseTeamRecord: Teams = { team1: 'A', team2: 'B', team3: 'C', team4: 'D' };

teamNumbers.forEach(teamNumber => {
  const k = `team${teamNumber}` as keyof Teams;
  console.log(verboseTeamRecord[k]);
});

这里是编辑的Typescriptplayground

鉴于您的 TeamRange 类型是模板文字字符串后的 union of numeric literal types, you can get your desired behavior simply by using a const assertion

const idx = `team${num}` as const;
/* const idx: "team1" | "team2" | "team3" | "team4" | "team5" | "team6" |
     "team7" | "team8" | "team9" | "team10" | "team11" | "team12" | 
    "team13" | "team14" | "team15" | "team16" */

然后编译器将您的模板文字字符串解释为具有 template literal type,它会自动将数字文字转换为字符串文字并为您连接字符串。所以一切都按照你想要的方式进行:

const name = record[`team${num}` as const]; // string, not any
name.toUpperCase(); // okay
name.blargh; // error

请注意,将有一个功能,即像 team${num} 这样的模板文字字符串会 自动 被推断为具有模板文字类型而不是 string ,这样你就不需要写 as const。这是在 microsoft/TypeScript#41891.

中实现的

但事实证明,这破坏了现有的现实世界代码库(特别是 Google,参见 microsoft/TypeScript#42593) and was deemed to be too weird (see discussion in microsoft/TypeScript#42589) and ultimately reverted in microsoft/TypeScript#42588

所以现在,如果您希望编译器将模板文字 string 视为具有模板文字 type,您我们需要通过添加 const 断言来告诉它。

Playground link to code

虽然不完美,但有一种方法可以利用 一些 类型检查甚至超过模板文字 属性 键。 TypeScript 的类型检查器检查代码的范围是有限的。只要代码使用模板文字,TypeScript 就无法执行 自动 类型检查,但有机会通过断言每个部分的类型来使用不仅仅是简单的类型断言模板文字的类型与预期的相同。这是一个 example:

type TeamNumber = 1 | 2 | 3 | 4;

type Team = 'team';

type VerboseTeamRecord = Teams & {
  id: number;
  event: string;
  gender: 'M' | 'F' | 'B';
};

type Teams = {
  [K in `${Team}${TeamNumber}`]: string;
};

var verboseTeamRecord: VerboseTeamRecord = { team1: 'A', team2: 'B', team3: 'C', team4: 'D', id: 1, event: 'foo', gender: 'M', };

const max: TeamNumber = 4;
const prefix: Team = 'team';

for (let i: TeamNumber = 1; i <= max; i++) {
  console.log(verboseTeamRecord[`${prefix}${i}` as keyof Teams]);
}

使用这种代码模式,有些错误是不可能的。虽然 max 可以简单地替换为 5 以产生错误,但 max 不能替换为 5 而不会出错 before runtime.