【问题标题】:Template literal key has an 'any' type because expression of type 'string' can't be used to index type模板文字键具有“任何”类型,因为“字符串”类型的表达式不能用于索引类型
【发布时间】:2021-10-25 12:53:09
【问题描述】:

关于如何断言特定键属于特定接口或类型有许多类似的问题,例如question。我的问题不同,因为有问题的密钥由两部分组成,而不是简单地使用来自 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 的类型?我不想要一个错误,等等。我的一些代码可以重写以使用现有属性,而其他代码则从那些使用名称和数字组合索引的现有属性创建新属性。

【问题讨论】:

  • 您能否提供一个minimal reproducible example 的代码,以在将您的问题按原样放入 TypeScript Playground 等独立 IDE 时演示您的问题?
  • 据我所知,解决方法可能很简单,只需将as const 放入其中,如this,但真的很高兴看到recordnum 的一些定义。
  • 我添加了代码。这是否有助于澄清我是如何尝试输入检查的?

标签: typescript mapped-types


【解决方案1】:

您可以使用众所周知的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

【讨论】:

  • 您映射类型的建议有效。还是原来的问题:typescriptlang.org/play?#code/…
  • 我明白了。我更新了我的答案。它对你有用吗?
  • 那行得通。我认为该策略使用了 TypeScript 实际上可以做的尽可能多的类型检查。对部分记录使用type Teams = { [K in '${Team}${TeamNumber}']: string; }; 可以实现更大的类型检查和更少的类型断言。
【解决方案2】:

鉴于您的 TeamRange 类型是数字 literal typesunion,您只需在模板文字字符串后使用 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)并被认为太奇怪了(请参阅 microsoft/TypeScript#42589 中的讨论)并最终在 microsoft/TypeScript#42588 中恢复。

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

Playground link to code

【讨论】:

    【解决方案3】:

    虽然不完美,但有一种方法可以利用 some 类型检查,甚至可以对模板文字属性键进行检查。 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 而不会出现错误之前运行时。

    【讨论】:

      猜你喜欢
      • 2021-03-17
      • 2021-04-20
      • 1970-01-01
      • 2020-11-13
      • 2019-12-17
      • 2020-10-29
      • 2020-10-04
      • 2019-12-15
      • 1970-01-01
      相关资源
      最近更新 更多