Детская площадка (раскомментируйте закомментированную строку в интерфейсе, чтобы увидеть ошибку, но также обратите внимание, как тип arg
в последней строке fun2
изменяется с комментированной строкой и без нее)
type ComponentName = 'A' | 'B' | 'C' | 'D';
type FullComponents = {
[P in ComponentName]: {};
}
type Components = Partial<FullComponents>;
export type ComponentsWith<T extends ComponentName> = Components & {
[P in T]: {};
};
interface EntityWith<T extends ComponentName> extends Entity {
//components: ComponentsWith<T>;
getComponent<U extends ComponentName>(name: U): ComponentsWith<T>[U];
setComponent<U extends ComponentName>(name: U, component: FullComponents[U]): asserts this is EntityWith<T | U>;
removeComponent<U extends ComponentName>(name: U): asserts this is EntityWith<Exclude<T, U>>;
}
class Entity {
public components: Components = {};
public getComponent<T extends ComponentName>(name: T) {
return this.components[name];
}
public setComponent<T extends ComponentName>(name: T, component: FullComponents[T]): asserts this is EntityWith<T> {
this.components[name] = component;
}
public removeComponent<T extends ComponentName>(name: T): asserts this is Entity {
this.components[name] = undefined;
}
}
Учитывая приведенный выше код установки, я ожидаю, что следующая функция:
function fun1(arg: Entity): EntityWith<'A'>{
arg.setComponent('B', {});
return arg; // 1: should Err
}
приведет к ошибке (поскольку типы возврата getComponent()
должны быть несовместимы?), Однако Это не. По какой-то причине EntityWith<T>
совместим с EntityWith<U>
для любых T
и U
, которые я пробовал (даже never
)
Поскольку у меня были повторяющиеся проблемы с обработкой TypeScript Сигнатуры функций (очень плохо), моя интуиция заключалась в том, что создание Entity.components
publi c и включение его в интерфейс может помочь. Так как Typescript правильно обрабатывает ComponentsWith<T>
(по крайней мере, во всех случаях, которые я тестировал), наличие свойства должно (как я предполагал) решить проблему.
Хотя это исправило мою первоначальную проблему, оно каким-то образом сломало asserts
в сигнатурах моей функции утверждения.
Следующее:
function fun2(arg: Entity): EntityWith<'A'>{
arg.setComponent('A', {});
arg.removeComponent('A');
return arg; // 2: arg should be EntityWith<never>
}
ранее приводило к тому, что значение arg было в порядке: Entity
, EntityWith<'A'>
, EntityWith<never>
, но после добавив свойство components
к интерфейсу, removeComponent()
больше не сужает тип, в результате чего он остается как EntityWith<'A'>
.
Может кто-нибудь объяснить, почему это происходит (для дальнейшего моего понимания типа TypeScript system) и, если возможно, предложите какой-нибудь способ заставить его работать так, как вам нужно?
Редактировать: Таким образом, главная проблема, безусловно, заключается в том, что функции утверждений не должны расширять тип. Я все еще хотел бы ответить на вопрос, почему в исходном коде они действительно расширяют тип, но, возможно, это ошибка? И я бы хотел какой-то обходной путь, если таковой существует (я пытался установить never
вместо расширения, но never
можно назначить для всех типов, так что это не помогает. И я не могу удалить removeComponent
начиная с EntityWith<T>
, так как тогда он неправильно расширяется Entity
Edit2: я думал об этом и исключал ошибку в TypeScript, единственное объяснение, которое я могу придумать, это то, что без свойства components
, EntityWith<T>
всегда считается сужением любого другого EntityWith<U>
независимо от того, что такое T
и U
. Повторные set
и remove
Components
корректно изменяют тип (пока вы не раскомментируете .components
. I проверено и EntityWith<T> & EntityWith<U>
не разрешает и не объединяет каким-либо образом, так что типы не так или иначе пересекаются. Это ошибка или ограничение дизайна того, как TypeScript обрабатывает функции? И добавление свойства как-то дает мне «правильное» "поведение?