【问题标题】:How can I iterate over Record keys in a proper type-safe way?如何以适当的类型安全方式迭代 Record 键?
【发布时间】:2023-03-20 13:15:01
【问题描述】:

让我们在一些属性上构建一个示例记录。

type HumanProp = 
    | "weight"
    | "height"
    | "age"

type Human = Record<HumanProp, number>;

const alice: Human = {
    age: 31,
    height: 176,
    weight: 47
};

对于每个属性,我还想添加一个人类可读的标签:

const humanPropLabels: Readonly<Record<HumanProp, string>> = {
    weight: "Weight (kg)",
    height: "Height (cm)",
    age: "Age (full years)"
};

现在,使用这个记录类型和定义的标签,我想迭代两个具有相同键类型的记录。

function describe(human: Human): string {
    let lines: string[] = [];
    for (const key in human) {
        lines.push(`${humanPropLabels[key]}: ${human[key]}`);
    }
    return lines.join("\n");
}

但是,我收到一个错误:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Readonly<Record<HumanProp, string>>'.
  No index signature with a parameter of type 'string' was found on type 'Readonly<Record<HumanProp, string>>'.

如何在 Typescript 中正确实现此功能?

澄清一下,我正在寻找的解决方案,无论是否使用Record 类型、普通对象、类型、类、接口或其他东西,都应该具有以下属性:

  1. 当我想定义一个新的属性时,我只需要在一个地方(如上面的 HumanProp 中)进行,不要重复自己。

  2. 在我定义一个新属性之后,我应该为这个属性添加一个新值的所有地方,比如当我创建alicehumanPropLabels 时,都会亮起类型错误 在编译时,而不是在运行时错误。

  3. 在我创建新属性时,迭代所有属性的代码(如 describe 函数)应该保持不变。

是否有可能用 Typescript 的类型系统实现类似的东西?

【问题讨论】:

标签: typescript


【解决方案1】:

我认为正确的方法是创建一个不可变的键名数组并为其指定一个窄类型,以便编译器将其识别为包含string literal types 而不仅仅是string。使用const assertion 最简单:

const humanProps = ["weight", "height", "age"] as const;
// const humanProps: readonly ["weight", "height", "age"]

那么你可以用它来定义HumanProp

type HumanProp = typeof humanProps[number];

你的其余代码应该或多或少地按原样工作,除了当你迭代键时你应该使用上面的不可变数组而不是Object.keys()

type Human = Record<HumanProp, number>;

const alice: Human = {
   age: 31,
   height: 176,
   weight: 47
};

const humanPropLabels: Readonly<Record<HumanProp, string>> = {
   weight: "Weight (kg)",
   height: "Height (cm)",
   age: "Age (full years)"
};

function describe(human: Human): string {
    let lines: string[] = [];
    for (const key of humanProps) { // <-- iterate this way
        lines.push(`${humanPropLabels[key]}: ${human[key]}`);
    }
    return lines.join("\n");
}

不使用Object.keys() 的原因是编译器无法验证Human 类型的对象是否 具有Human 中声明的键。 TypeScript 中的对象类型是开放的/可扩展的,而不是封闭的/exact。这允许接口扩展和类继承工作:

interface SuperHero extends Human {
   powers: string[];
}
declare const captainStupendous: SuperHero;
describe(captainStupendous); // works, a SuperHero is a Human

您不希望describe() 爆炸,因为您传入了SuperHero,这是一种特殊类型的Human,带有一个额外的powers 属性。因此,与其使用正确生成string[]Object.keys(),不如使用已知属性的硬编码列表,这样describe() 这样的代码将忽略任何存在的额外属性。


而且,如果您向humanProps 添加一个元素,您会在所需位置看到错误,而describe() 将保持不变:

const humanProps = ["weight", "height", "age", "shoeSize"] as const; // added prop

const alice: Human = { // error! 
   age: 31,
   height: 176,
   weight: 47
};

const humanPropLabels: Readonly<Record<HumanProp, string>> = { // error!
   weight: "Weight (kg)",
   height: "Height (cm)",
   age: "Age (full years)"
};

function describe(human: Human): string { // okay
   let lines: string[] = [];
   for (const key of humanProps) {
      lines.push(`${humanPropLabels[key]}: ${human[key]}`);
   }
   return lines.join("\n");
}

好的,希望对您有所帮助;祝你好运!

Playground link to code

【讨论】:

  • 当我尝试将此解决方案与产品类型一起使用时,我遇到了一些问题。如果我定义单独的HumanSuper 类型,我将如何遍历type SuperHuman = Human &amp; Super 类型属性?
  • 没关系,想通了:const superHumanProps = ([] as SuperHumanProp[]).concat(humanProps, superHumanProps);
【解决方案2】:

有时你必须弯腰才能使用 Typescript……其他时候最好让 Typescript 为你弯腰。我会选择比尝试维护一些具体的字符串数组实例(并使其与接口保持同步)更简单的方法。我不会使用HumanProps。在我看来,这个简单的更改(添加as keyof Human)是避免错误的最佳方法(IMO 甚至使代码更具可读性):

function describe2(human: Human): string {
    let lines: string[] = [];
    for (const key in human) {
        lines.push(`${humanPropLabels[key as keyof Human]}: ${human[key as keyof Human]}`);
    }
    return lines.join("\n");
}

【讨论】:

  • 还记得人们说过 Javascript 的动态类型可以为我们节省很多时间吗?没那么多。我在奇怪的 Typescript 问题上花了多少小时...?
【解决方案3】:
// instead of
Record<number, MyType>

// might be better to use
Map<number, MyType>

// then you can iterate over the map without
// javascript casting all your keys to strings

const m = new Map<number, string>();

m.forEach((value: string, key: number) =>
 console.log(`m[${key}]=${value}`)
);

【讨论】:

  • 虽然此代码可能会回答问题,但提供有关它如何和/或为什么解决问题的额外上下文将提高​​答案的长期价值。您可以在帮助中心找到更多关于如何写好答案的信息:stackoverflow.com/help/how-to-answer。祝你好运?
猜你喜欢
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 2013-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-20
  • 2013-07-10
相关资源
最近更新 更多