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?
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?
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
}
}