如何以适当的类型安全方式迭代 Record 键?

Max*_*kov 13 typescript

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

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

type Human = Record<HumanProp, number>;

const alice: Human = {
    age: 31,
    height: 176,
    weight: 47
};
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

但是,我收到一个错误:

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>>'.
Run Code Online (Sandbox Code Playgroud)

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

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

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

  2. 在我定义了一个新属性之后,我应该为这个属性添加一个新值的所有地方,比如在我创建时alice,或者humanPropLabels,在编译时点亮类型错误,而不是在运行时错误中。

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

甚至可以使用 Typescript 的类型系统来实现类似的功能吗?

jca*_*alz 15

我认为这样做的正确方法是创建一个不可变的键名数组并为其指定一个窄类型,以便编译器将其识别为包含字符串文字类型而不是string. 这是最简单的const断言

const humanProps = ["weight", "height", "age"] as const;
// const humanProps: readonly ["weight", "height", "age"]
Run Code Online (Sandbox Code Playgroud)

然后你可以HumanProp用它来定义:

type HumanProp = typeof humanProps[number];
Run Code Online (Sandbox Code Playgroud)

其余代码应该或多或少按原样工作,除了当您迭代键时,您应该使用上面的不可变数组而不是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");
}
Run Code Online (Sandbox Code Playgroud)

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

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

您不想describe()爆炸,因为您传入了SuperHero,这是Human一种具有额外powers属性的特殊类型。因此,Object.keys()与其使用which 正确产生string[],您应该使用已知属性的硬编码列表,以便代码 likedescribe()将忽略任何存在的额外属性。


并且,如果您向 中添加元素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");
}
Run Code Online (Sandbox Code Playgroud)

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

Playground 链接到代码


Che*_*les 6

对于上面的这个具体问题,提供了很好的答案,但是,就我而言,我的错误是首先尝试使用记录类型。

退一步来说,如果您尝试将键映射到值并以类型安全的方式迭代它们,那么最好使用 Map:

type Test = 'harry' | 'sam' | 'alex';

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

for (const [k, v] of m)
{
    // this won't compile because k is type Test
    if (k === 'foo') { console.log(v); }
}
Run Code Online (Sandbox Code Playgroud)

这样做的缺点是,与 Record 不同,Map 不能保证类型 Test 中的所有键都已映射......但我最终以新手的身份进入此页面,试图找出如何迭代 Record<number, string >,显然我的错误是一开始就选择了 Record 类型。

无论如何,这种见解对我很有帮助。也许它会帮助别人。

  • 虽然此代码可以回答问题,但提供有关如何和/或为何解决问题的附加上下文将提高​​答案的长期价值。您可以在帮助中心找到有关如何编写良好答案的更多信息:https://stackoverflow.com/help/how-to-answer。祝你好运 (2认同)