Тип поля класса на основе универсального - PullRequest
3 голосов
/ 25 января 2020

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

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 []' is нельзя назначить типу «AvailableTypes [T]».
тип «never []» нельзя назначить типу «never»

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

... и

Свойство 'value' не имеет инициализатора и не определено в конструкторе .

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

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

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

Обновление

(обновление до @ jcalz answer ) Должна быть возможность проверить тип 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);
    }
}

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

1 Ответ

2 голосов
/ 25 января 2020

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

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

Та же проблема вызывает ошибку "value не определено назначен". У компилятора нет средств, чтобы увидеть, что switch / case исчерпывает все возможности для 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];
    }
}

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


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

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


Обновление: дискриминированный союз вместо универсальных c классов

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...