Dan Stevens
Dan Stevens

Reputation: 6830

How to initialize a property of a mapped typed in Typescript

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 FruitShop1 interface to use a mapped types based off Fruit:

interface FruitShop2 {
  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

Upvotes: 1

Views: 556

Answers (3)

Dan Stevens
Dan Stevens

Reputation: 6830

I've gone for a similar solution as suggested by @soffyo, but instead of using an array I'm using an enum backed by enums. Here's my code:

enum Fruits {
  Apple = "apple",
  Banana = "banana",
  Cherry = "cherry"
}

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

class FruitShop implements IFruitShop {
  prices: IFruitShop['prices'];
  stock: IFruitShop['stock'];

  constructor() {
    this.prices = {} as { [fruit in Fruits]: number };
    this.stock = {} as { [fruit in Fruits]: number };
    Object.keys(Fruits).forEach(f => {
      this.prices[f as Fruits] = 1;
      this.stock[f as Fruits] = 0;
    });
  }
}

Typscript Playground

I think string enum could be an advantage for me in my actual project.

Upvotes: 1

asnaeb
asnaeb

Reputation: 745

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

Upvotes: 1

A_A
A_A

Reputation: 1932

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.

Upvotes: 1

Related Questions