Вывод универсального Typescript из статического свойства

#typescript #typescript-generics

#typescript #typescript-generics

Вопрос:

Я пишу библиотеку, в которой пользователь может определять подклассы абстрактного класса State . Любой подкласс State будет «полагаться» (в рамках логики библиотеки) на ряд классов, унаследованных от абстрактного класса Component . Эти зависимости должны быть объявлены в коде статически, чтобы моя библиотека могла анализировать их заранее. Я хотел бы также проверить методы State унаследованных пользовательских классов на основе зависимостей, которые они определяют. Вот пример:

 class ComponentA extends Component {}
class ComponentB extends Component {}

class ExampleState extends State {
  static dependencies = [ComponentA, ComponentB] as const;

  // this method should correctly type the tuple provided to this
  // user-defined method as containing an instance of ComponentA
  // in position 0 and an instance of ComponentB in position 1, ordered as
  // provided in the static dependencies property.
  initialize(components) {
    const [a, b] = components;
    console.assert(a instanceof ComponentA); // true
  }
}
 

В частности, я хотел бы избежать принуждения пользователя также передавать общий State тип like State<[typeof ComponentA, typeof ComponentB]> , который был бы как подробным, так и избыточным. Однако я не могу полагаться только на общий, потому что мне нужна информация о зависимостях во время выполнения.

Есть ли какие-либо варианты для определения этого в TypeScript? Или статика просто слишком отделена от экземпляров, чтобы иметь возможность делиться информацией об этом типе? Существует ли другой шаблон (возможно, помимо классов), который мог бы обеспечить правильную эргономику разработчика для этого использования?

Обновить

После долгих экспериментов я обнаружил по крайней мере одно возможное использование, которое поддерживает эргономику (подсказки типа для initialize метода в соответствии с указанными зависимостями), не требуя избыточности в коде:

Ссылка на игровую площадку TS

Тем не менее, использование все еще немного неудобно. Пользователь должен предоставить функцию для создания своего объекта псевдокласса и использовать замыкание для хранения «свойств экземпляра», которые они обычно просто присоединяют к своему подклассу.

Мне все еще интересно посмотреть, может ли кто-нибудь предложить более упрощенное решение, но если нет, я сам отвечу, по крайней мере, этим, что работает.

Обновление 2: более подробная информация о моей цели

Я работаю над библиотекой фреймворков для создания веб-игр, которая вдохновлена / расширяет шаблон ECS для разработки игр. Часть, которую я хочу добавить в ECS, — это способ для пользователей фреймворка или авторов дополнительных библиотек определять декларативное управление состоянием на основе определенных комбинаций компонентов, присоединенных к какой-либо конкретной сущности.

Как пользователь, пишущий игру с фреймворком, я хочу включить шаблон типа «Если к какой-либо сущности [Image, Position, Color] в любой момент жизненного цикла игры подключены компоненты, Sprite фреймворк должен создавать объект «Состояние», который извлекает свои данные из этих компонентов».

Для достижения этой цели пользователь предоставит определения состояний фреймворку, которые реализуют initialize destroy интерфейс / . Эти определения состояний будут отвечать за предоставление императивной логики для обеспечения декларативного использования в логическом коде игры.

Вот набросок использования, которое я бы в идеале хотел включить, используя гипотетический SomeSpriteThing ресурс, аналогичный чему-то из PixiJS или ThreeJS, например:

 class SpriteState extends State {
  static dependencies = [Image, Position, Color] as const;

  private sprite = new SomeSpriteThing();

  initialize([image, position, color]) {
    this.sprite.src = image.src;
    this.sprite.x = position.x;
    this.sprite.y = position.y;
    this.sprite.tint = color.value;
    this.sprite.load();
  }

  destroy() {
    this.sprite.unload();
  }
}
 

Пользователь библиотеки будет предоставлять такие классы состояний фреймворку, и всякий раз, когда сущности назначаются [Image, Position, Color] компоненты вместе, фреймворк SpriteState будет генерировать a и предоставлять доступ к логическому коду игры.

Однако в идеальном использовании, приведенном выше, initialize не было бы способа вывести типизацию предоставленного кортежа из dependencies статического.

Как библиотека, ориентированная на Typescript, я хочу, чтобы код, который пишут пользователи, тщательно проверялся без чрезмерных усилий с их стороны при указании избыточных типов. Вот почему я хотел бы избежать принуждения пользователя предоставлять один и тот же список типов зависимостей для общего параметра в State суперклассе — это не только неудобно, но и невозможно обеспечить dependencies синхронизацию общего и общего, поскольку один виден только для кода, а другой — дляпроверка типов.

Я признаю, что это не тот шаблон, который я видел раньше, и, возможно, он сильно переработан. Возможно, это все, что нужно. Но мне было любопытно, есть ли какой-то возможный способ приблизиться к этому с помощью безопасности типов.

Ответ №1:

Есть ли какие-либо варианты для определения этого в TypeScript? Или статика просто слишком отделена от экземпляров, чтобы иметь возможность делиться информацией об этом типе?

Когда вы создаете класс ExampleState , вы также создаете тип ExampleState , который описывает экземпляр этого класса. Статические свойства не применяются к ExampleState , поскольку они не являются свойствами экземпляра. У каждого класса также есть второй тип, typeof ExampleState или new () => ExampleState , который описывает конструктор класса. Здесь определяются статические свойства. (соответствующий раздел документации)

Существует ли другой шаблон (возможно, помимо классов), который мог бы обеспечить правильную эргономику разработчика для этого использования?

Я рекомендую использовать какую-то функцию более высокого порядка для создания ваших классов. Мне трудно понять это точно, потому что я не до конца понимаю вариант использования, но это должно указать вам, по крайней мере, правильное направление.

Этот интерфейс описывает конструктор, который мы хотим. Его можно создать для создания экземпляра State<Deps> , и он также имеет свойство dependencies type Deps .

 interface StateCreator<Deps extends ComponentType<any>[]> {
    new (): State<Deps>;
    dependencies: Deps;
}
 

Эта функция принимает ваши зависимости и возвращает анонимный класс, который имеет эти зависимости в качестве статических свойств.

 function makeState<Deps extends ComponentType<any>[]>(dependencies: Deps): StateCreator<Deps> {
    return class extends State<Deps> {
        static dependencies: Deps = dependencies;

        initialize(components: Deps) {
            //do something
        }
    }
}
 

Я немного не понимаю, в чем заключается ваше намерение initialize . Зачем нам указывать Deps в качестве аргумента, если Deps он уже известен из статических свойств? Вы имели в виду, что для этого нужно использовать экземпляры ComponentA и ComponentB ?

Комментарии:

1. Я прошу прощения, в своих правках, чтобы сократить мой пример кода до самого необходимого, я действительно случайно перепутал экземпляры компонентов и конструкторы. initialize получает экземпляры компонентов, конструкторы которых предоставляются как зависимости. Я обновлю вопрос. Мое желаемое использование в качестве библиотеки — иметь возможность фактически определять результирующий класс (который наследуется от State), указывать конструкторы компонентов, от которых он зависит статически, и получать проверку типов в их экземплярах по мере их передачи initialize , которые определяет пользователь.

2. Чтобы уточнить, я думаю, что подход factory является хорошим (у меня есть аналогичная идея в моем обновлении по этому вопросу). Но одна из моих целей — заставить TS автоматически предлагать и применять ввод для initialize метода, как его определяет пользователь . Библиотека предоставляет инструменты, помогающие пользователю определять подклассы состояния, которые являются типобезопасными в соответствии с их dependencies , но пользователь определяет класс в конце. На английском языке намерение выглядит так: «Я хочу определить вид состояния, которое зависит от этих компонентов, и реализует это initialize для набора экземпляров этих компонентов».

3. Я думаю, что я не совсем «понимаю» это. Моя общая склонность к композиции вместо наследования. Я не уверен, от какой функциональности мы наследуем State и можем ли мы использовать композиционный шаблон, чтобы объединить эту функциональность с той initialize , которую мы хотим определить.

4. Вероятно, я либо не смог предоставить достаточно контекста, либо моя цель слишком перегружена. Тем не менее, я добавил более подробную информацию о специфике того, что я пытаюсь создать, к вопросу, если вам интересно. Я думаю, что мой желаемый шаблон использования, вероятно, невозможен с TS… Я ценю, что вы все равно занимаетесь этим вопросом. Если то, что я добавил, понятно, и ответ «вы не можете этого сделать», вы можете добавить это в свой ответ, и я приму это, учитывая, что ваши другие предложения уже были полезны для переосмысления проблемы.

5. Итак, я немного поиграл с этим, потому что мне действительно нравится думать о дизайне. В этом коде все еще есть проблемы, но движется ли он в правильном направлении? tsplay.dev/GmZl9w