Home > Net >  Adding a property to an object causes a TypeScript error
Adding a property to an object causes a TypeScript error

Time:09-28

I have struck what appears to be an obscure error in my TypeScript code that has me completely stumped, and searching the docs and Internet hasn't uncovered the reason. I have included some fictitious code below that demonstrates the problem that I have encountered in my real project. In this example code, the issue is around the "animals" object at the end of the code and the "for" loop that follows it.

With reference to the full code that is included at the end, if the animals object is as follows then there are no TypeScript errors.:

let current = "carnivores";

const animals = {
  carnivores: [wildCarnivores, domesticCarnivores,],
  herbivores: [wildHerbivores, domesticHerbivores]
}

for (let animal of animals[current as keyof typeof animals][0].table) {
  console.log(`name: ${animal.name}; weight: ${animal.weight}`)

However, if I add any other property to the "animals" object as follows:

let current = "carnivores";

const animals = {
  something: "hi",
  carnivores: [wildCarnivores, domesticCarnivores,],
  herbivores: [wildHerbivores, domesticHerbivores]
}

for (let animal of animals[current as keyof typeof animals][0].table) {
  console.log(`name: ${animal.name}; weight: ${animal.weight}`)
}

then I get the following TypeScript error:

Property 'table' does not exist on type 'string | AnimalCategoryInterface'.     Property 'table' does not exist on type 'string'.

As an additional bit of information that may give a clue to the cause, if I hardcode "carnivores" (instead of using the value of the "current" variable) in the "for" loop then there are no TypeScript errors:

let current = "carnivores";

const animals = {
  something: "hi",
  carnivores: [wildCarnivores, domesticCarnivores,],
  herbivores: [wildHerbivores, domesticHerbivores]
}

for (let animal of animals["carnivores"][0].table) {
  console.log(`name: ${animal.name}; weight: ${animal.weight}`)

The full source code is:

interface AnimalInterface {
  name: string,
  weight: number,
}

interface AnimalCategoryInterface {
  table: AnimalInterface[],
}

const wildCarnivores: AnimalCategoryInterface = {
  table: [
    {
      name: "Lion",
      weight: 150,
    },
    {
      name: "Polar Bear",
      weight: 400,
    }
  ]
};

const domesticCarnivores: AnimalCategoryInterface = {
  table: [
    {
      name: "Cat",
      weight: 4,
    },
    {
      name: "Dog",
      weight: 20,
    }
  ]
};

const wildHerbivores: AnimalCategoryInterface = {
  table: [
    {
      name: "Deer",
      weight: 80,
    },
    {
      name: "Panda",
      weight: 100,
    }
  ]
};

const domesticHerbivores: AnimalCategoryInterface = {
  table: [
    {
      name: "Guinea Pig",
      weight: 1,
    },
    {
      name: "Horse",
      weight: 300,
    }
  ]
};

let current = "carnivores";

const animals = {
  something: "hi",
  carnivores: [wildCarnivores, domesticCarnivores,],
  herbivores: [wildHerbivores, domesticHerbivores]
}

for (let animal of animals[current as keyof typeof animals][0].table) {
  console.log(`name: ${animal.name}; weight: ${animal.weight}`)
}

for (let animal of animals["carnivores"][0].table) {
  console.log(`name: ${animal.name}; weight: ${animal.weight}`)
}

Playground link

CodePudding user response:

When you have something: "hi", in animals, then animals[current as keyof typeof animals] is no longer an array type, since you've added a property of type string. So now the type of animals[current as keyof typeof animals] is string | AnimalCategoryInterface[], not just AnimalCategoryInterface[]. But you're not handling that possibility in your code. Although animals[current as keyof typeof animals][0] remains valid either way (its type is string | AnimalCategoryInterface, since you can index into strings using [0]), but the .table part is invalid because strings don't have a table property.

Instead, make sure you have an AnimalCategoryInterface instance before trying to use it as an array, by adding a type guard:

const element = animals[current as keyof typeof animals];
if (typeof element !== "string") { // <== type guard
    for (let animal of element[0].table) {
        console.log(`name: ${animal.name}; weight: ${animal.weight}`);
    }
}

Playground link

CodePudding user response:

I think you need split it into two functions. First one will check whether current is allowed, second one will iterate it.

interface AnimalInterface {
    name: string;
    weight: number;
}

interface AnimalCategoryInterface {
    table: AnimalInterface[];
}

const wildCarnivores: AnimalCategoryInterface = {
    table: [
        {
            name: "Lion",
            weight: 150,
        },
        {
            name: "Polar Bear",
            weight: 400,
        },
    ],
};

const domesticCarnivores: AnimalCategoryInterface = {
    table: [
        {
            name: "Cat",
            weight: 4,
        },
        {
            name: "Dog",
            weight: 20,
        },
    ],
};

const wildHerbivores: AnimalCategoryInterface = {
    table: [
        {
            name: "Deer",
            weight: 80,
        },
        {
            name: "Panda",
            weight: 100,
        },
    ],
};

const domesticHerbivores: AnimalCategoryInterface = {
    table: [
        {
            name: "Guinea Pig",
            weight: 1,
        },
        {
            name: "Horse",
            weight: 300,
        },
    ],
};

let current = "carnivores";

const animals = {
    something: "hi",
    carnivores: [wildCarnivores, domesticCarnivores],
    herbivores: [wildHerbivores, domesticHerbivores],
};

type Animals = typeof animals

type Values<T> = T[keyof T]

type TupleProperty<T> = Values<{
    [Prop in keyof T]: T[Prop] extends any[] ? Prop : never
}>

const isTupleProperty = <
    Data extends Record<PropertyKey, unknown>
>(data: Data, key: PropertyKey): key is TupleProperty<Data> =>
    key in data && Array.isArray(data[key])

const iterate = (data: Animals, current: TupleProperty<Animals>) => {
    for (let animal of data[current][0].table) {
        console.log(`name: ${animal.name}; weight: ${animal.weight}`);
    }
}

iterate(animals, 'herbivores') // ok
iterate(animals, 'something') // expected error

let key = 'any string'

if(isTupleProperty(animals,key)){
    iterate(animals, key) // ok
}

Playground

TupleProperty - returns keys which are corresponds to array values

isTupleProperty - is custom typeguard which checks whether variable is allowed to use (whether key coresponds to array)

As you might have noticed, something is not allowed value for iterate function


Pros: You don't use type assertions at all

Cons: you have extra function instead of condition statement

P.S. Rule of thumb: if you have condition statement, think whether it can be transformed to custom typeguard. IT does not mean that each condition statement should be transformed, it should be at least considered

  • Related