Home > Back-end >  How to initialize a property of a mapped typed in Typescript
How to initialize a property of a mapped typed in Typescript

Time:08-22

Let's say I want to model a Fruit Shop in Typescript: I have a union type of string literals:

type Fruit = "apple" | "banana" | "cherry"

I have a fruit shop with a price list and stock register:

interface FruitShop1 {
  priceList: {
    apple: number,
    banana: number,
    cherry: number
  }
  stock: {
    apple: number,
    banana: number,
    cherry: number
  }
}

Since I will want to add lots of fruit later and I don't want to duplicate the properties manually on the priceList and stock properties, so I modified the FruitCost interface to use a mapped types based off Fruit:

interface FruitCost {
  priceList: { [fruit in Fruit]: number }
  stock: { [fruit in Fruit]: number }
}

I create my class implemeting FruitShop:

class FruitShopImpl implements FruitShop2 {
  priceList: { apple: number; banana: number; cherry: number; };
  stock: { apple: number; banana: number; cherry: number; };
}

However, I need to initialize pricelist and stock. How do I do this?

enter image description here

What I'd like is for every property in priceList be initialized to 1 and every property in stock to be initialized to 0.

I've tried interating over every property in a constructor, but this produces errors I think because priceList isn't initialized so it has no properties to iterate over. This feels like a chicken-and-egg catch-22 problem. Is there a solution, without manually initializing each property individually?

enter image description here

CodePudding user response:

You cannot use the type to construct the object at runtime. However, you can define an array of fruits and derive the Fruit type from that. Then you can later on use this array to initialize it at runtime with Array.reduce.

However, I didn't manage to do it without the type assertion in the reduce call, as we initialize it with {} which should have the type Partial<FruitCost> and only at the end really is of type FruitCost.

const fruits = ['apple', 'banana'] as const

type Fruits = (typeof fruits)[number]

interface IFruitCost {
  prices: { [fruit in Fruits]: number }
  stock: { [fruit in Fruits]: number }
}

class FruitCost implements IFruitCost {
  prices: FruitCost['prices']
  stock: FruitCost['stock']

  constructor() {
    this.prices = fruits.reduce((prices, fruit) => {
      prices[fruit] = 1
      return prices
    }, {} as FruitCost['prices'])

    this.stock = fruits.reduce((stock, fruit) => {
      stock[fruit] = 0
      return stock
    }, {} as FruitCost['stock'])
  }
}

Here is the playground link.

CodePudding user response:

You cannot use a type to produce runtime values, so you have to create a real array

const Fruit = [
    "apple" as const, 
    "banana" as const, 
    "cherry" as const
]

Using as const assertion, you are assigning a definite value as a type which can be used by other types

interface FruitCost {
  priceList: { [k in typeof Fruit extends (infer f)[] ? f : never]: number }
  stock: { [k in typeof Fruit extends (infer f)[] ? f : never]: number }
}

Now you can initialise priceList and stock as empty objects and add properties to them in the constructor

class FruitShopImpl implements FruitCost {
  priceList = {} as { [k in typeof Fruit extends (infer f)[] ? f : never]: number }
  stock = {} as { [k in typeof Fruit extends (infer f)[] ? f : never]: number }
  constructor() {
    for (const k of Fruit) {
        this.priceList[k] = 1
        this.stock[k] = 0
    }
  }
}

This will produce

FruitShopImpl: {
  "priceList": {
    "apple": 1,
    "banana": 1,
    "cherry": 1
  },
  "stock": {
    "apple": 0,
    "banana": 0,
    "cherry": 0
  }
} 

Playground link

  • Related