typescript 如何以正确的类型安全方式迭代Record键?

at0kjp5o  于 2023-02-25  发布在  TypeScript
关注(0)|答案(3)|浏览(204)

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

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)"
};

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

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),而不需要重复。
1.定义了一个新属性后,所有应该为该属性添加新值的地方,比如创建alicehumanPropLabels时,都会在编译时而不是运行时显示类型错误
1.当我创建一个新属性时,迭代所有属性的代码,比如describe函数,应该保持不变。
用Typescript的类型系统实现这样的东西有可能吗?

6tdlim6h

6tdlim6h1#

我认为正确的方法是创建一个键名称的不可变数组,并给予它一个窄类型,这样编译器就可以识别它包含字符串类型,而不仅仅是string,使用constAssert最容易做到这一点:

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中的对象类型是open/extendible,而不是closed/exact。这允许接口扩展和类继承工作:

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

你不希望describe()因为你传入了一个SuperHero而爆炸,SuperHeroHuman的一种特殊类型,它有一个额外的powers属性,所以你不应该使用Object.keys(),它会正确地产生string[],你应该使用已知属性的硬编码列表,这样像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代码链接

a0zr77ik

a0zr77ik2#

有时候你不得不弯曲于使用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");
}
camsedfj

camsedfj3#

对于上面这个特定的问题,已经有了很好的答案,但是,在我的例子中,我的错误是一开始就试图使用记录类型。
退一步说,如果你试图将键Map到值,并以类型安全的方式迭代它们,你最好使用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); }
}

// if you just want to iterate over keys
for (const k of m.keys())
{
    console.log(m.get(k));
}

或者说,解决我的,简单得多的,问题的方法是:

// using a map I can iterate over the map without
// all my keys being coerced to strings

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

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

不管怎么说,这个洞察力帮助了我。也许它也能帮助别人。

相关问题