Тип поля класса на основе универсального

Представьте себе следующий код (детская площадка):

type AvailableTypes = {
    'array': Array<any>;
    'string': string;
    'object': object;
}

class Wrapper<T extends keyof AvailableTypes> {

    // Is either array, string or object
    private readonly type: T;

    // ERROR: Property 'value' has no initializer and is not definitely assigned in the constructor.
    private readonly value: AvailableTypes[T];

    constructor(type: T) {
        this.type = type;

        /**
         * ERROR:
         *  TS2322: Type 'never[]' is not assignable to type 'AvailableTypes[T]'.   
         *  Type 'never[]' is not assignable to type 'never'.
         */
        switch (type) {
            case 'array':
                this.value = [];
                break;
            case 'string':
                this.value = '';
                break;
            case 'object':
                this.value = {};
                break;
        }
    }
}

Есть две основные ошибки:

TS2322: Тип 'never []' нельзя назначить типу 'AvailableTypes [T]'.
Тип 'never []' нельзя назначить типу 'never'

Даже если AvailableTypes[T] всегда разрешается в один из типов, объявленных в AvailableTypes, где T является его ключом.

... а также

Свойство value не имеет инициализатора и точно не назначается в конструкторе.

Хотя type является обязательным и должно быть либо string, array, либо object.

Что мне здесь не хватает?

Возможные связанные темы SO:

Обновлять

(обновление до ответа @jcalz) Должна существовать возможность проверки типа value на основе свойства type:

// In the Wrapper class, should work since value can only be an array if type is 'array':
public pushValue(val: unknown) {
    if (this.type === 'array') {
        this.value.push(val);
    }
}

Детская площадка


person Simon    schedule 25.01.2020    source источник


Ответы (1)


Основная проблема заключается в том, что параметры универсального типа не сужаются с помощью анализа потока управления, что является довольно давней открытой проблемой в TypeScript, см. microsoft / TypeScript # 24085 для получения дополнительной информации.

Когда вы проверяете type в инструкции _2 _ / _ 3_, компилятор может сузить тип переменной type до буквального типа "array", но не сузит параметр типа T до "array". И поэтому он не может проверить, безопасно ли присвоить значение any[] типу AvailableTypes[T]. Компилятору придется провести некоторый анализ, который он в настоящее время не выполняет, например, хорошо, если type === "array", и мы вывели T из типа type, то внутри этого блока case мы можем сузить T до "array", и, следовательно, тип this.value будет AvailableTypes["array"], он же any[], поэтому присвоить ему [] безопасно. Но этого не происходит.

Эта же проблема вызывает ошибку value не определенно назначена. У компилятора нет средств, чтобы увидеть, что _21 _ / _ 22_ исчерпывает все возможности для T, поскольку он не выполняет здесь анализ потока управления.


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

Чтобы справиться с проблемой полноты, вы можете создать default прецедент, который бросает, как в:

class Wrapper<T extends keyof AvailableTypes> {

    private readonly type: T;
    private readonly value: AvailableTypes[T];
    constructor(type: T) {
        this.type = type;

        switch (type) {
            case 'array':
                this.value = [] as AvailableTypes[T]; // assert
                break;
            case 'string':
                this.value = '' as AvailableTypes[T]; // assert
                break;
            case 'object':
                this.value = {} as AvailableTypes[T]; // assert
                break;
            default:
                throw new Error("HOW DID THIS HAPPEN"); // exhaustive
        }
    }
}

или вы можете расширить type с T до keyof AvailableTypes, что позволит компилятору выполнить анализ потока управления, необходимый ему, чтобы понять, что случаи являются исчерпывающими:

class Wrapper<T extends keyof AvailableTypes> {
    private readonly type: T;
    private readonly value: AvailableTypes[T];
    constructor(type: T) {
        this.type = type;
        const _type: keyof AvailableTypes = type; // widen to concrete type
        switch (_type) {
            case 'array':
                this.value = [] as AvailableTypes[T]; // assert
                break;
            case 'string':
                this.value = '' as AvailableTypes[T]; // assert
                break;
            case 'object':
                this.value = {} as AvailableTypes[T]; // assert
                break;
        }
    }
}

Другой обходной путь (упомянут в комментарии человека, который реализовал изменение устойчивости ) состоит в том, чтобы воспользоваться тем фактом, что если у вас есть значение t типа T и ключ k типа K extends keyof T, тогда тип t[k] будет рассматриваться компилятором как T[K]. Итак, если мы можем создать действительный AvailableTypes, мы можем просто проиндексировать его с помощью type. Может быть так:

class Wrapper<T extends keyof AvailableTypes> {
    private readonly type: T
    private readonly value: AvailableTypes[T];
    constructor(type: T) {
        this.type = type;
        const initValues: AvailableTypes = {
            array: [],
            string: "",
            object: {}
        };
        this.value = initValues[type];
    }
}

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


Хорошо, надеюсь, что это поможет. Удачи!

площадка ссылка на код


обновление: размеченное объединение вместо общих классов

person jcalz    schedule 25.01.2020
comment
Спасибо за подробный ответ! Но когда я пытаюсь проверить, является ли type array, и пытаюсь вставить что-то в значение, это не удается :( Даже если type в основном строго привязан к свойству value ... - person Simon; 25.01.2020
comment
Я не вижу этого в вопросе; я пропустил это? Или это отдельный или дополнительный вопрос? Также всегда помогает минимальный воспроизводимый пример. Удачи! - person jcalz; 26.01.2020
comment
Извините, я обновил вопрос - свойство type действительно предназначалось для этой цели! - person Simon; 26.01.2020
comment
Проблема та же, и единственное решение, которое я вижу без полного рефакторинга, - это утверждение типа: (this.value as AvailableTypes["array"]).push(val);. Если бы я знал, что вы планируете делать такие манипуляции внутри реализации Wrapper, я бы посоветовал вам вместо этого использовать размеченное объединение и либо не использовать классы, либо использовать семейство подклассов (по одному подклассу для каждого члена объединения). - person jcalz; 26.01.2020
comment
Я добавил ссылку на игровую площадку внизу своего ответа, показывающую, как размеченное объединение с родственными классами может работать лучше, чем один общий класс. удачи! - person jcalz; 26.01.2020
comment
@jcalz Большое спасибо за отличное объяснение и ссылку на проблему. Я несколько раз сталкивался с подобными проблемами, но не мог найти никакой полезной информации. Дискриминационный союз - очень умное решение - person Valeriy Katkov; 26.01.2020