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

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

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 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.

    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
}

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:

确定