英文:
TypeScript: Filling an Object progressively to match a type/interface
问题
在TypeScript中,是否有一种逐步构建对象以匹配类型或接口的方法,而不会因为从一开始就没有所有预期属性而导致IDE(或编译器?)抱怨?
让我举个例子。我有一组水果,并且我想要操作将值分配给每个水果的对象。
type Fruit = 'apple' | 'lemon' | 'melon';
type FruitMapToString = { [key in Fruit]: string; };
const FRUIT_DESCRIPTIONS: FruitMapToString = {
  apple: 'A basic fruit',
  lemon: 'An acidic fruit',
  melon: 'A refreshing fruit',
} as const;
这是一个可能会修改以添加新水果的文件。我不希望我的代码的其余部分依赖于存在哪些水果的知识,因为我不想更新它。
现在,我想要一个从水果到数字的映射(用于计算我的水果数量)。我想要使用一种类型,该类型暗示所有水果都被包含在内:
type FruitMapToNumber = { [key in Fruit]: number; };
因此,我希望依赖于FRUIT_DESCRIPTIONS的键来初始化我的对象为:{ apple: 0, lemon: 0, melon: 0 }。但我无法找到一种方法来做到这一点,而不会在某个时候抱怨某些属性缺失。
通常情况下,我想这样做:
function makeFruitToNumberMap(): FruitMapToNumber {
  const map: FruitMapToNumber = {}; // ❌ 缺少属性 'apple' 等
  let fruit: Fruit;
  for (fruit in FRUIT_DESCRIPTIONS) {
    map[fruit] = 0;
  }
  return map;
}
但它不起作用,因为当初始化为{}时,我的映射缺少属性。
我还尝试过从键是可选的类型开始:
const map: { [key in Fruit]?: boolean; } = {};
但然后IDE在return级别抱怨,因为“'number | undefined'不能分配给类型'number'”。(尽管在函数结束时,所有水果都被分配为0。)
我甚至尝试了这个,尽管我不想为了可读性而使用它:
function makeFruitToNumberMap(): FruitMapToNumber {
  return Object.fromEntries(
    Object.keys(FRUIT_DESCRIPTIONS).map(
      (fruit) => [fruit, 0]
    )
  );
}
再次,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.
type Fruit = 'apple' | 'lemon' | 'melon';
type FruitMapToString = { [ key in Fruit ]: string; };
const FRUIT_DESCRIPTIONS: FruitMapToString = {
  apple: 'A basic fruit',
  lemon: 'An acidic fruit',
  melon: 'A refreshing fruit',
} 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:
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:
function makeFruitToNumberMap(): FruitMapToNumber {
  const map: FruitMapToNumber = {}; // ❌ Missing the properties 'apple' etc
  let fruit: Fruit;
  for ( fruit in FRUIT_DESCRIPTIONS ) {
    map[ fruit ] = 0;
  }
  return map;
}
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:
  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:
function makeFruitToNumberMap(): FruitMapToNumber {
  return Object.fromEntries(
    Object.keys( FRUIT_DESCRIPTIONS ).map(
      ( fruit ) => [ fruit, 0 ]
    )
  );
}
Again, the IDE complains about missing properties.
答案1
得分: 0
以下是代码的中文翻译:
// 可以考虑在这里使用枚举来设置主要类型并用于迭代:
enum Fruit {
  apple = "苹果",
  lemon = "柠檬",
  melon = "西瓜"
}
type TFruit = keyof typeof Fruit; // 与您的字符串联合类型相同
type FruitMapToNumber = { [key in Fruit]: number };
type FruitMapToString = { [key in Fruit]: string };
const FRUIT_DESCRIPTIONS: FruitMapToString = {
  [Fruit.apple]: "一种基础水果",
  [Fruit.lemon]: "一种酸性水果",
  [Fruit.melon]: "一种清新的水果"
};
// 无法创建具有预定义字段的对象。因此,我们只能通过迭代来创建它。但这里有两个不愉快的事情:
//  - 第一个是因为它是迭代创建,我们总是从一个**空**对象开始,这意味着最终会有`Partial`类型。
//  - 这里的第二个不愉快之处是,js不允许我们传递哪些键将从`Object.keys`方法返回,它总是期望返回`string`键。
function makeFruitToNumberMap() {
  const keys = Object.keys(Fruit); // 第二个问题:键的类型是string[]
  const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); // 第一个问题:原始对象为空,我们逐步设置值
  return map;
}
// 因此,为了解决这个问题,我们可以使用**类型断言**:
function makeFruitToNumberMap() {
  const keys = Object.keys(Fruit) as TFruit[];
  const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
  return map as FruitMapToNumber;
}
希望这对您有所帮助!如果您有任何其他问题,请随时问。
英文:
It might be more convinient to use enum here for setting up primary types and use them for iterating:
enum Fruit {
  apple = "apple",
  lemon = "lemon",
  melon = "melon"
}
type TFruit = keyof typeof Fruit; //it's the same as your strings uion type
type FruitMapToNumber = { [key in Fruit]: number };
type FruitMapToString = { [key in Fruit]: string };
const FRUIT_DESCRIPTIONS: FruitMapToString = {
  [Fruit.apple]: "A basic fruit",
  [Fruit.lemon]: "An acidic fruit",
  [Fruit.melon]: "A refreshing fruit"
};
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
Partialtype in the end. - 
the 2d unpleasant thing here is that js doesn't allow us to
pass which keys will be returned fromObject.keysmethod, it always
expects to returnstringkeys.function makeFruitToNumberMap() { const keys = Object.keys(Fruit); // 2d issue: keys type is string[] const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {}); // 1st issue: original object is empty and we set values iteratively return map; } 
So, to resolve it we can use casting
function makeFruitToNumberMap() {
  const keys = Object.keys(Fruit) as TFruit[];
  const map = keys.reduce((acc, key) => ({ ...acc, [key]: 0 }), {});
  return map as FruitMapToNumber;
}
答案2
得分: 0
Anastasiya Stanevich 让我意识到我所缺少的是类型断言,即使用 … as Type 来断言我的对象确实是所需的类型 Type。
我还应该使用 Partial<Type> 来为我的“尚未完成”的对象定义类型。
通常,在我提供的示例中:
function makeFruitToNumberMap(): FruitMapToNumber {
  const map: Partial<FruitMapToNumber> = {}; // 👈 部分
  let fruit: Fruit;
  for (fruit in FRUIT_DESCRIPTIONS) {
    map[fruit] = 0;
  }
  return map as FruitMapToNumber; // 👈 类型断言
}
英文:
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:
function makeFruitToNumberMap(): FruitMapToNumber {
  const map: Partial<FruitMapToNumber> = {}; // 👈 Partial
  let fruit: Fruit;
  for ( fruit in FRUIT_DESCRIPTIONS ) {
    map[ fruit ] = 0;
  }
  return map as FruitMapToNumber; // 👈 Type assertion
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论