Как в typescript создать массив с разными классами

Хотелось бы иметь возможность хранить некие объекты с разными своими дополнениями в одном списке юнитов на карте. Там могут быть разные животные унаследованные от какого-то общего класса Animal (да и кто-то еще).

В юнити я бы это сделал простым списком List-Gameobject, и потом проверял компоненты

Dog dog = itemObj.GetComponent<Dog>();
if (dog != null) dog.woof("Отдай колбасу")

Я не понимаю как сделать подобный подход в ts. Максимум могу создать массив any[], но потом я не могу обращаться к его элементам как к понятным сущностям, а только как к any.


Ответы (2 шт):

Автор решения: ksa

Как в typescript создать массив с разными классами

Такое можно сделать, например, вот так...

class Animal {
    name: string
    constructor(name: string){
        this.name = name
    }
}
class Dog extends Animal {
    //
}
class Cat extends Animal {
    //
}
type Test = Dog | Cat
const arr: Test[] = [
    new Dog('Шарик'),
    new Cat('Мурзик')
]
arr.forEach(o => {
    if (o instanceof Dog) console.log('Это собака, имя ', o.name) 
})
→ Ссылка
Автор решения: Швеев Алексей

Так же, как и в unity придётся использовать явное приведение типов.

Вы можете реализовать систему компонентов как в юнити, но для маленьких проектов всё таки реализовать разную логику через ООП.

Реализация разных сущностей через ООП

Первым делом определим сам мир (который хранит все сущности) и саму основу сущности:

type vec2 = [number, number]

class World {
  // Одна и таже сущность всё ещё является одной и той же сущностью
  // И не может храниться в списке дважды или более раз
  // Поэтому используем Set
  // (+ оптимизация при удалении и поиске)
  entities: Set<Entity> = new Set();

  // Заспавнить сущность
  spawn(entity: Entity) {
    this.entities.add(entity);
    entity.world = this;
    entity.onSpawn();
    return entity;
  }

  // Уничтожить сущность
  destroy(entity: Entity) { 
    entity.onDestroy();
    entity.world = undefined;
    this.entities.delete(entity);
  }
  
  // Обновить все сущности
  update() {
    this.entities.forEach((entity) => {
      entity.onUpdate();
    });
  }
}

abstract class Entity {
  // Мир, в котором находится сущность
  world?: World;
  position: vec2 = [0, 0];
  
  // Когда сущность спавниться (можно override)
  onSpawn() {}
  
  // Когда сущность уничтожается (можно override)
  onUpdate() {}

  // Уничтожить сущность
  destroy() {
    if (this.world) {
      this.world.destroy(this);
    }
  }

  // Когда сущность уничтожается (можно override)
  onDestroy() {}

  // Жива ли сущность
  isAlive() {
    return this.world !== undefined;
  }
}d?: World;
  position: vec2 = [0, 0];
  
  start() {}
  update() {}

  destroy() {
    if (this.world) {
      this.world.destroy(this);
    }
  }
}

Наследовав от класса Entity мы теперь можем создавать новые типы сущностей. Например: создадим подтип сущности Animal, который будет представлять животных, что издают звуки на каждом onUpdate:

abstract class Animal extends Entity {
  abstract says: string;

  override onUpdate() {
    super.onUpdate();
    console.log(this.says);
  }
}

Попрошу обратить внимания, что мы реализовали разную логику в двух разных классах, но до сих пор ещё не разу не проверяли на тип самой сущности. Эта тема затронута более подробно чуть ниже

Ну и теперь для примера создадим 2 простых типа сущности Animal:

class Cow extends Animal {
  says: string = "mooo";
}
class Dog extends Animal {
  says: string = "woof"
  breed: string; // Порода собаки

  constructor(breed: string = "default") {
    super();
    this.breed = breed;
  }
}

Вот пример того, как это можно использовать:

let world = new World();

let cow1 = world.spawn(new Cow());
let cow2 = world.spawn(new Cow());
let dog1 = world.spawn(new Dog("служебная"))

world.update();
// console: 
// mooo 
// mooo 
// woof

Допустим мы имплементировали новый метод для Entity: onCollide(...), который вызывается, когда this сущность сталкивается с другой сущностью.

Мы хотим, что бы любой экземпляр Cow реагировал на собак, но только определённой (охотничьей) породы.

Но увы: наша функция onCollide(...) даёт нам Entity. Что бы проверить, является ли данная сущность собакой и узнать её породу можно воспользоваться оператором instanceof, который позволяет проверить, является ли объект экземпляром указанного класса:

class Cow extends Animal {
  says: string = "mooo";

  onCollide(other: Entity) {
    if (other instanceof Dog // Так как мы уже проверили наш Entity на необходимый тип
        && other.breed === "охотничая") // Typescript уже знает, что тут other является экземпляром Dog
    {
      console.log("*runs away*")
    }
  }
}

Плейграунд с этим кодом

Однако как я уже говорил - если есть возможность, то лучше подобные проверки минимизировать.

В данном случае лучше оставить так, однако если необходимое поведение можно реализовать через ООП, то лучше так и сделать. Пример:

Вместо

class Updatable extends Entity {
  update() {...}
}

...

if (entity instanceof Updatable) {
  entity.update(); // существует только в типе Updatable, нужна проверка
}

Лучше сделать

class Entity {
  update() {}
}

class SomeOtherClass extends Entity {
  override update() {...}
}

...

entity.update(); // работает на любом entity
→ Ссылка