Почему наличие свойства в интерфейсе TypeScript влияет на сужение типов с помощью утверждений и как я могу заставить его работать правильно? - PullRequest
4 голосов
/ 29 апреля 2020

Детская площадка (раскомментируйте закомментированную строку в интерфейсе, чтобы увидеть ошибку, но также обратите внимание, как тип 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 обрабатывает функции? И добавление свойства как-то дает мне «правильное» "поведение?

...