TypeScript:逐步填充对象以匹配类型/接口

huangapple go评论100阅读模式
英文:

TypeScript: Filling an Object progressively to match a type/interface

问题

在TypeScript中,是否有一种逐步构建对象以匹配类型或接口的方法,而不会因为从一开始就没有所有预期属性而导致IDE(或编译器?)抱怨?

让我举个例子。我有一组水果,并且我想要操作将值分配给每个水果的对象。

  1. type Fruit = 'apple' | 'lemon' | 'melon';
  2. type FruitMapToString = { [key in Fruit]: string; };
  3. const FRUIT_DESCRIPTIONS: FruitMapToString = {
  4. apple: 'A basic fruit',
  5. lemon: 'An acidic fruit',
  6. melon: 'A refreshing fruit',
  7. } as const;

这是一个可能会修改以添加新水果的文件。我不希望我的代码的其余部分依赖于存在哪些水果的知识,因为我不想更新它。

现在,我想要一个从水果到数字的映射(用于计算我的水果数量)。我想要使用一种类型,该类型暗示所有水果都被包含在内:

  1. type FruitMapToNumber = { [key in Fruit]: number; };

因此,我希望依赖于FRUIT_DESCRIPTIONS的键来初始化我的对象为:{ apple: 0, lemon: 0, melon: 0 }。但我无法找到一种方法来做到这一点,而不会在某个时候抱怨某些属性缺失。

通常情况下,我想这样做:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. const map: FruitMapToNumber = {}; // ❌ 缺少属性 'apple' 等
  3. let fruit: Fruit;
  4. for (fruit in FRUIT_DESCRIPTIONS) {
  5. map[fruit] = 0;
  6. }
  7. return map;
  8. }

但它不起作用,因为当初始化为{}时,我的映射缺少属性。

我还尝试过从键是可选的类型开始:

  1. const map: { [key in Fruit]?: boolean; } = {};

但然后IDE在return级别抱怨,因为“'number | undefined'不能分配给类型'number'”。(尽管在函数结束时,所有水果都被分配为0。)

我甚至尝试了这个,尽管我不想为了可读性而使用它:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. return Object.fromEntries(
  3. Object.keys(FRUIT_DESCRIPTIONS).map(
  4. (fruit) => [fruit, 0]
  5. )
  6. );
  7. }

再次,IDE抱怨缺少属性。

英文:

In TypeScript, is there a way to progressively build an object to match a type or interface, without having the IDE (or the compiler?) complaining because not all expected attributes are here from the start?

Let me take an example. I have a list of fruits, and I will want to manipulate objects that assign values to each and every fruit.

  1. type Fruit = 'apple' | 'lemon' | 'melon';
  2. type FruitMapToString = { [ key in Fruit ]: string; };
  3. const FRUIT_DESCRIPTIONS: FruitMapToString = {
  4. apple: 'A basic fruit',
  5. lemon: 'An acidic fruit',
  6. melon: 'A refreshing fruit',
  7. } as const;

This is in a file that might be modified to add new fruits. And I don't want the rest of my code to rely on the knowledge of what fruits exists, because I don't want to update it.

Now, I want a map from fruits to number (to count my fruits). And I want to use a type that implies that all fruits are covered:

  1. type FruitMapToNumber = { [ key in Fruit ]: number; };

So, I want to rely on FRUIT_DESCRIPTIONS's key to initialize my object to: { apple: 0, lemon: 0, melon: 0 }. But I can't find a way to do that without the IDE complaining at some point that some properties are lacking.

Typically I would like to do that:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. const map: FruitMapToNumber = {}; // ❌ Missing the properties 'apple' etc
  3. let fruit: Fruit;
  4. for ( fruit in FRUIT_DESCRIPTIONS ) {
  5. map[ fruit ] = 0;
  6. }
  7. return map;
  8. }

But it doesn't work, because when initialized to {}, my map is lacking properties.

I also tried starting with a type where the keys are optional:

  1. const map: { [ key in Fruit ]?: boolean; } = {};

But then the IDE complains at the return level, because “'number | undefined' is not assignable to type 'number'”. (Even thought at the end of the functions, all fruits are assigned to 0.)

I even tried this, even through I don't want to use that for readability:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. return Object.fromEntries(
  3. Object.keys( FRUIT_DESCRIPTIONS ).map(
  4. ( fruit ) => [ fruit, 0 ]
  5. )
  6. );
  7. }

Again, the IDE complains about missing properties.

答案1

得分: 0

以下是代码的中文翻译:

  1. // 可以考虑在这里使用枚举来设置主要类型并用于迭代:
  2. enum Fruit {
  3. apple = "苹果",
  4. lemon = "柠檬",
  5. melon = "西瓜"
  6. }
  7. type TFruit = keyof typeof Fruit; // 与您的字符串联合类型相同
  8. type FruitMapToNumber = { [key in Fruit]: number };
  9. type FruitMapToString = { [key in Fruit]: string };
  10. const FRUIT_DESCRIPTIONS: FruitMapToString = {
  11. [Fruit.apple]: "一种基础水果",
  12. [Fruit.lemon]: "一种酸性水果",
  13. [Fruit.melon]: "一种清新的水果"
  14. };
  15. // 无法创建具有预定义字段的对象。因此,我们只能通过迭代来创建它。但这里有两个不愉快的事情:
  16. // - 第一个是因为它是迭代创建,我们总是从一个**空**对象开始,这意味着最终会有`Partial`类型。
  17. // - 这里的第二个不愉快之处是,js不允许我们传递哪些键将从`Object.keys`方法返回,它总是期望返回`string`键。
  18. function makeFruitToNumberMap() {
  19. const keys = Object.keys(Fruit); // 第二个问题:键的类型是string[]
  20. const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); // 第一个问题:原始对象为空,我们逐步设置值
  21. return map;
  22. }
  23. // 因此,为了解决这个问题,我们可以使用**类型断言**:
  24. function makeFruitToNumberMap() {
  25. const keys = Object.keys(Fruit) as TFruit[];
  26. const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
  27. return map as FruitMapToNumber;
  28. }

希望这对您有所帮助!如果您有任何其他问题,请随时问。

英文:

It might be more convinient to use enum here for setting up primary types and use them for iterating:

  1. enum Fruit {
  2. apple = "apple",
  3. lemon = "lemon",
  4. melon = "melon"
  5. }
  6. type TFruit = keyof typeof Fruit; //it's the same as your strings uion type
  7. type FruitMapToNumber = { [key in Fruit]: number };
  8. type FruitMapToString = { [key in Fruit]: string };
  9. const FRUIT_DESCRIPTIONS: FruitMapToString = {
  10. [Fruit.apple]: "A basic fruit",
  11. [Fruit.lemon]: "An acidic fruit",
  12. [Fruit.melon]: "A refreshing fruit"
  13. };

There's no way to create an object with already predefined fields. So, we can create it only by iterating. And here there're 2 unpleasant things:

  • the 1st one as it's iterating creation we always start with an empty object and that means we have Partial type in the end.

  • the 2d unpleasant thing here is that js doesn't allow us to
    pass which keys will be returned from Object.keys method, it always
    expects to return string keys.

    1. function makeFruitToNumberMap() {
    2. const keys = Object.keys(Fruit); // 2d issue: keys type is string[]
    3. const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); // 1st issue: original object is empty and we set values iteratively
    4. return map;
    5. }

So, to resolve it we can use casting

  1. function makeFruitToNumberMap() {
  2. const keys = Object.keys(Fruit) as TFruit[];
  3. const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
  4. return map as FruitMapToNumber;
  5. }

答案2

得分: 0

Anastasiya Stanevich 让我意识到我所缺少的是类型断言,即使用 … as Type 来断言我的对象确实是所需的类型 Type

我还应该使用 Partial<Type> 来为我的“尚未完成”的对象定义类型。

通常,在我提供的示例中:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. const map: Partial<FruitMapToNumber> = {}; // 👈 部分
  3. let fruit: Fruit;
  4. for (fruit in FRUIT_DESCRIPTIONS) {
  5. map[fruit] = 0;
  6. }
  7. return map as FruitMapToNumber; // 👈 类型断言
  8. }
英文:

Anastasiya Stanevich made me realize that what I was lacking is type assertion, i.e., using … as Type to assert that my object was indeed, at this point, of the required type Type.

I should also be using Partial<Type> to type my “not yet completed” object.

Typically, in the sample case I provided:

  1. function makeFruitToNumberMap(): FruitMapToNumber {
  2. const map: Partial<FruitMapToNumber> = {}; // 👈 Partial
  3. let fruit: Fruit;
  4. for ( fruit in FRUIT_DESCRIPTIONS ) {
  5. map[ fruit ] = 0;
  6. }
  7. return map as FruitMapToNumber; // 👈 Type assertion
  8. }

huangapple
  • 本文由 发表于 2023年6月12日 17:43:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76455390.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定