Battling TypeScript Types

I have been struggling with this typescript issue for a bit now and not sure how to fix. I have an object containing some data that I import. It looks like this. boom has a crazy long type considering each of the objects in the array. (where I think this is making things hard).
export const boom = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
},
},
{
name: "C4",
cost: {
sulfur: 2200,
charcoal: 3000,
frags: 200,
low_grade: 60,
cloth: 5,
tech_trash: 2,
},
},
{
name: "Satchel",
cost: {
sulfur: 480,
charcoal: 720,
frags: 80,
cloth: 10,
rope: 1,
},
},
{
name: "Explosive Ammo",
cost: {
sulfur: 25,
charcoal: 30,
frags: 5,
},
},
{
name: "Beancan Grenade",
cost: {
sulfur: 120,
charcoal: 180,
frags: 20,
},
},
{
name: "Incendiary Rocket",
cost: {
sulfur: 610,
charcoal: 900,
frags: 10,
low_grade: 253,
pipes: 2,
},
},
{
name: "High Velocity Rocket",
cost: {
sulfur: 200,
charcoal: 300,
pipes: 1,
},
},
{
name: "Handmade Shell",
cost: {
sulfur: 5,
charcoal: 8,
stone: 3,
},
},
];
export const boom = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
},
},
{
name: "C4",
cost: {
sulfur: 2200,
charcoal: 3000,
frags: 200,
low_grade: 60,
cloth: 5,
tech_trash: 2,
},
},
{
name: "Satchel",
cost: {
sulfur: 480,
charcoal: 720,
frags: 80,
cloth: 10,
rope: 1,
},
},
{
name: "Explosive Ammo",
cost: {
sulfur: 25,
charcoal: 30,
frags: 5,
},
},
{
name: "Beancan Grenade",
cost: {
sulfur: 120,
charcoal: 180,
frags: 20,
},
},
{
name: "Incendiary Rocket",
cost: {
sulfur: 610,
charcoal: 900,
frags: 10,
low_grade: 253,
pipes: 2,
},
},
{
name: "High Velocity Rocket",
cost: {
sulfur: 200,
charcoal: 300,
pipes: 1,
},
},
{
name: "Handmade Shell",
cost: {
sulfur: 5,
charcoal: 8,
stone: 3,
},
},
];
The type for each item is automatically created, but I would rather it just be name: string and cost: Record<string, number>. How can I just simplify the types? My app works fine but I just cant get rid of this typescript error. Thanks!
33 Replies
exodus
exodus17mo ago
for complex types like this, you should declare the type rather than infering. This is a good case for types/interfaces. I'll go with Record<string, number> for now for cost, but you really should also define these in a union type or enum, assuming their names are known ahead of time.
interface Explosive {
name: string;
cost: Record<string, number>;
}

// or if you prefer
type Explosive = {
name: string,
cost: Record<string, number>,
}

export const boom: Explosive[] = [
{
name: "Handmade Shell",
cost
..etc
}
]
interface Explosive {
name: string;
cost: Record<string, number>;
}

// or if you prefer
type Explosive = {
name: string,
cost: Record<string, number>,
}

export const boom: Explosive[] = [
{
name: "Handmade Shell",
cost
..etc
}
]
This kind of sounds like a game, so I'd probably opt for interfaces over types. Games are one situations where Object Oriented patterns can be useful
Perfect
Perfect17mo ago
Ahh this helped a lot. “Declare instead of infer”. Could you give me a sample of what u meant by defining a union type or enum? I import this array of items, then map the array and pass the item to a component. In the component I type the item the same as Explosive in ur example and it works better. However, how can u be more specific? (There is nothing I have to specifically do when it’s a certain item)
exodus
exodus17mo ago
Assuming there are known keys for the items in cost, then it would be good to define them.
interface Explosive {
name: string;
cost: Record<Material, number>;
}

type Material = "sulfur" | "charcoal" | "frags"
interface Explosive {
name: string;
cost: Record<Material, number>;
}

type Material = "sulfur" | "charcoal" | "frags"
Or maybe Material is another object you'd like to define
interface Explosive {
name: string;
cost: Record<typeof Material[name], number>; //I think this is right
}

interface Material {
name: MaterialName
cost: number
}

type MaterialName = "sulfur" | "charcoal" | "frags"
interface Explosive {
name: string;
cost: Record<typeof Material[name], number>; //I think this is right
}

interface Material {
name: MaterialName
cost: number
}

type MaterialName = "sulfur" | "charcoal" | "frags"
Alternatively, you can use an enum, even though these are a bit out of style today. They're still probably more appropriate in this case:
interface Explosive {
name: string;
cost: Record<MaterialName, number>; //I think this is right
}

enum MaterialName {
SULFUR = "sulfur",
CHARCOAL = "charcoal",
FRAGS = "frags"
}
interface Explosive {
name: string;
cost: Record<MaterialName, number>; //I think this is right
}

enum MaterialName {
SULFUR = "sulfur",
CHARCOAL = "charcoal",
FRAGS = "frags"
}
You can get as crazy as you want, really. Just think of any place where your IDE doesn't provide you proper autocompletion, and then see if there might be a better way to define your types. In this case, Record<string, number> is probably too broad. If you replace string with a union or enum, then you'll actually get autocomplete and type checking when you try to add a property to cost.
Perfect
Perfect17mo ago
Cool, I will try to get a little more specific!
Perfect
Perfect16mo ago
@exodus so I gave ur idea a shot and I am getting a strange error.
type Item =
| "sulfur"
| "charcoal"
| "frags"
| "low_grade"
| "cloth"
| "stone"
| "pipes"
| "tech_trash"
| "rope";

type Explosive = {
name: string;
cost: Record<Item, number>;
};

export const boom: Explosive[] = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
}
}
]
type Item =
| "sulfur"
| "charcoal"
| "frags"
| "low_grade"
| "cloth"
| "stone"
| "pipes"
| "tech_trash"
| "rope";

type Explosive = {
name: string;
cost: Record<Item, number>;
};

export const boom: Explosive[] = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
}
}
]
barry
barry16mo ago
man is playing rust
Perfect
Perfect16mo ago
Check it out!! haha https://rust-meta.vercel.app/raiding/calculator But in regards to that error, I am not sure why its expecting each cost object to have every single item, when the type for item is not requiring that
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
barry
barry16mo ago
yh partial
Perfect
Perfect16mo ago
Ohh ok, I did not think that was needed because of the | I think of that as OR but probably shouldnt
barry
barry16mo ago
yh no its mapping it going through them all and doing
low_grade, number
cloth, number
etc...
low_grade, number
cloth, number
etc...
Perfect
Perfect16mo ago
export const boom: Explosive[] = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
},
},
{
name: "C4",
cost: {
sulfur: 2200,
charcoal: 3000,
frags: 200,
low_grade: 60,
cloth: 5,
tech_trash: 2,
},
},
{
name: "Satchel",
cost: {
sulfur: 480,
charcoal: 720,
frags: 80,
cloth: 10,
rope: 1,
},
},
etc...
];
export const boom: Explosive[] = [
{
name: "Rocket",
cost: {
sulfur: 1400,
charcoal: 1950,
frags: 100,
low_grade: 30,
pipes: 2,
},
},
{
name: "C4",
cost: {
sulfur: 2200,
charcoal: 3000,
frags: 200,
low_grade: 60,
cloth: 5,
tech_trash: 2,
},
},
{
name: "Satchel",
cost: {
sulfur: 480,
charcoal: 720,
frags: 80,
cloth: 10,
rope: 1,
},
},
etc...
];
So sulfur and charcoal are in every item, so a better type for this would be something like
type Explosive = {
name: string;
cost: {
sulfur: number,
charcoal: number
} & Partial<Record<Item, number>>
};
type Explosive = {
name: string;
cost: {
sulfur: number,
charcoal: number
} & Partial<Record<Item, number>>
};
lets see if that even works 😆 looks like it did 👀 So now my object.entries is being annoying because its not typing the destructured key nicely
Object.entries(item.cost).forEach(([key, value]) => {
newTotalCost[key] = (newTotalCost[key] ?? 0) + value * count;
});
Object.entries(item.cost).forEach(([key, value]) => {
newTotalCost[key] = (newTotalCost[key] ?? 0) + value * count;
});
Perfect
Perfect16mo ago
Perfect
Perfect16mo ago
Perfect
Perfect16mo ago
anyone see how this is even possible? much appreciated lol
barry
barry16mo ago
lol
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
Ok so for option 1 (just gonna cast for now), that worked well.
type Explosive = {
name: string;
cost: {
sulfur: number;
charcoal: number;
} & Partial<Record<RaidItem, number>>;
};

type Cost = Explosive["cost"] | null;

function handleIncrement(count: number) {
const newAmount = amount + count;

if (totalCost !== null) {
const newTotalCost: Cost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Explosive["cost"], number];
newTotalCost[key] = (newTotalCost[key] ?? 0) + value * count;
});

setTotalCost(newTotalCost);
} else {
setTotalCost(item.cost);
}

setAmount(newAmount);
}
type Explosive = {
name: string;
cost: {
sulfur: number;
charcoal: number;
} & Partial<Record<RaidItem, number>>;
};

type Cost = Explosive["cost"] | null;

function handleIncrement(count: number) {
const newAmount = amount + count;

if (totalCost !== null) {
const newTotalCost: Cost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Explosive["cost"], number];
newTotalCost[key] = (newTotalCost[key] ?? 0) + value * count;
});

setTotalCost(newTotalCost);
} else {
setTotalCost(item.cost);
}

setAmount(newAmount);
}
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
I use it twice (for now) but I think the other undefined error is unrelated potentially. My increment adds to existing fields or adds entirely new ones. My decrement does the exact opposite (obv).
function handleDecremenent(count: number) {
const newAmount = amount - count;

if (amount > 0 && totalCost !== null) {
const newTotalCost: Cost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Explosive["cost"], number];
newTotalCost[key] = newTotalCost[key] - value * count; // Object is possibly 'undefined'.
if (newTotalCost[key] === 0) {
delete newTotalCost[key];
}
});

setAmount(newAmount);

if (Object.values(newTotalCost).every((x) => x === 0)) {
setTotalCost(null);
} else {
setTotalCost(newTotalCost);
}
}
}
function handleDecremenent(count: number) {
const newAmount = amount - count;

if (amount > 0 && totalCost !== null) {
const newTotalCost: Cost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Explosive["cost"], number];
newTotalCost[key] = newTotalCost[key] - value * count; // Object is possibly 'undefined'.
if (newTotalCost[key] === 0) {
delete newTotalCost[key];
}
});

setAmount(newAmount);

if (Object.values(newTotalCost).every((x) => x === 0)) {
setTotalCost(null);
} else {
setTotalCost(newTotalCost);
}
}
}
@kapobajza so now, I think typescript is basically telling me that the newTotalCost might not have the key that I am trying to decrement (even tho I know it will because this will only reach if amount > 0, which means that at least one of these items is part of the total cost). I think i need to find a way to tell typescript newTotalCost will definitely contain all of the keys I am traversing for the current item (item.cost)
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
ohh interesting How would u recommend I indicate that the cost is nothing? still use null like u showed there? My main state uses the type const [totalCost, setTotalCost] = useState<Cost>(null);
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
OHH I had it like that wayyyy earlier but thought I was making it cleaner by doing it all in the Cost type lmao, let me see how this works now, thank you!
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
Yep just changed that as well, I am still getting the undefined error somehow, might be something small
type Cost = Explosive["cost"];

type CalculatorItemProps = {
item: Explosive;
totalCost: Cost | null;
setTotalCost: Dispatch<SetStateAction<Cost | null>>;
};

function handleDecremenent(count: number) {
const newAmount = amount - count;

if (amount > 0 && totalCost !== null) {
const newTotalCost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Cost, number]; // tried keyof typeof totalCost here as well
newTotalCost[key] = newTotalCost[key] - value * count; // Object is possibly 'undefined'.
if (newTotalCost[key] === 0) {
delete newTotalCost[key];
}
});

setAmount(newAmount);

if (Object.values(newTotalCost).every((x) => x === 0)) {
setTotalCost(null);
} else {
setTotalCost(newTotalCost);
}
}
}
type Cost = Explosive["cost"];

type CalculatorItemProps = {
item: Explosive;
totalCost: Cost | null;
setTotalCost: Dispatch<SetStateAction<Cost | null>>;
};

function handleDecremenent(count: number) {
const newAmount = amount - count;

if (amount > 0 && totalCost !== null) {
const newTotalCost = { ...totalCost };

Object.entries(item.cost).forEach((arr) => {
const [key, value] = arr as [keyof Cost, number]; // tried keyof typeof totalCost here as well
newTotalCost[key] = newTotalCost[key] - value * count; // Object is possibly 'undefined'.
if (newTotalCost[key] === 0) {
delete newTotalCost[key];
}
});

setAmount(newAmount);

if (Object.values(newTotalCost).every((x) => x === 0)) {
setTotalCost(null);
} else {
setTotalCost(newTotalCost);
}
}
}
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
well that works, but is technically a little weird because it could go negative theoretically it should never but I have a feeling its not exactly correct
Unknown User
Unknown User16mo ago
Message Not Public
Sign In & Join Server To View
Perfect
Perfect16mo ago
@kapobajza resolved this madness by greatly reducing my state complexity. I now just store every item in the cost object starting at 0 and render the ones that are greater than 0. no errors and much much much more concise code
exodus
exodus16mo ago
AWesome. Yeah I should have pointed out that Record is usually something you reach for for dynamic keys...i.e. something that you don't know at compile time. Sorry, I don't know why i didn't go that route first