How to ensure every object property is set in a TypeScript function?

54 Views Asked by At

I am developing a 2D canvas game using TypeScript and using the Object pool pattern.

That is, when creating a new enemy, rather than always creating one like this:

interface IEnemy {
  x: number,
  y: number,
  health: number,
  maxHealth: number,
}

function spawnNewEnemy() {
  const newEnemy: IEnemy = {
    x: Math.random() * 100,
    y: 50,
    health: 100,
    maxHealth: 100
  };

  enemies.push(newEnemy);
}

I instead do this (to avoid an allocation with creating a new object, we instead just re-use an existing enemy):

function spawnNewEnemy() {
  const newEnemy = enemyPool.get();

  newEnemy.x = Math.random() * 100;
  newEnemy.y = 50;
  newEnemy.health = 100;
  newEnemy.maxHealth = 100;

  enemies.push(newEnemy);
}

However, the problem now is that I sometimes forget a property. Is there any way to ensure that every single property gets set in the new spawnNewEnemy function? Previously if I forgot one there would be a compiler error, but now there no longer is.

In addition, is there any way to use a single function that either creates a new enemy (allocates a new object) or overwrites an existing one? Because with this current approach I need two separate ones (one for initially allocating an enemy, and a separate one for overwriting an existing one), which is quite verbose and error-prone.

1

There are 1 best solutions below

1
David CM On

You could create a function that receives a never param like so:

function assertValidProperty(x: never): never {
    throw new Error("Invalid property");
}

And then loop over the object properties to set each one in a switch:

function spawnNewEnemy() {
  const newEnemy = enemyPool.get();

  newEnemy.x = Math.random() * 100;
  newEnemy.y = 50;
  newEnemy.health = 100;
  newEnemy.maxHealth = 100;

  const enemyProperties = Object.keys(newEnemy);

  for (const property of enemyProperties) {
    const key = property as keyof IEnemy;

    switch (key) {
      case "health":
        newEnemy[key] = 123;
        break;
      case "maxHealth":
        // ...
        break;
        case "x":
        // ...
        break;
        case "y":
        // ...
        break;
      default:
        return assertValidProperty(key);
    }
  }

  enemies.push(newEnemy);
}

The code should never reach the default branch, so if you miss a property to set in the switch case, you will se the following error:

Argument of type 'string' is not assignable to parameter of type 'never'

To answer the second question, you can first check if your enemies array contains the enemy you are searching (you could find by id, name, position, etc.) and then, create or update depending on the result:

if(enemies.find(enemy => enemy.some_property === the_property/*check you condition*/)){
    enemies.map(enemy => enemy.some_property === the_property ? updated_enemy  :enemy)
}else{
    enemies.push(new_enemy);
}

To update it you can do a map and only update if the object is the one you are looking for, or you could find the index of that object in the array and override it.