Как добавить охранников расширений к условным типам? Или эквивалент

Скажем, у меня есть класс с универсальным параметром. У него есть условные типы для некоторых свойств класса. Эти условные типы зависят от универсального параметра, являющегося одним из двух возможных значений перечисления. В конструкторе я передаю тип, который следует тем же границам типа, что и универсальный параметр. Я ожидаю, что проверка login_type === TwoChoices.REGISTER сузит тип универсального параметра класса T, однако этого не происходит. Вот пример.

enum TwoChoices {
    LOGIN,
    REGISTER
}

class ConditionalGenericClass<T extends TwoChoices> {
    password: string;
    email: string;
    username?: T extends TwoChoices.REGISTER ? string : never;
    constructor(login_type: T) {
        this.password = '';
        this.email = '';
        if (login_type === TwoChoices.REGISTER) {
            this.username = '';
        }
    }
}

Машинопись площадка ссылка

Вышеупомянутый бросает TypeError: Type '""' is not assignable to type '(T extends TwoChoices.REGISTER ? string : never) | undefined'..

Я ожидал, что смогу написать, это просто добавить защиту типа с C extends TwoChoices.REGISTER в качестве возвращаемого типа функции, например:

enum TwoChoices {
    LOGIN,
    REGISTER
}

function typeGuardExtendsTwoChoicesRegister<C extends TwoChoices>
    (login_type: C): C extends TwoChoices.REGISTER 
{
    return login_type === TwoChoices.REGISTER;
}

class ConditionalGenericClass<T extends TwoChoices> {
    password: string;
    email: string;
    username?: T extends TwoChoices.REGISTER ? string : never;
    constructor(login_type: T) {
        this.password = '';
        this.email = '';
        if (typeGuardExtendsTwoChoicesRegister(login_type)) {
            this.username = '';
        }
    }
}

Машинопись площадка ссылка

Однако похоже, что X extends Y не является допустимым возвращаемым типом защиты типа.

Каков правильный, лучший способ статического ввода таких сценариев динамического типа в JavaScript?

Вероятно, вы предложите использовать наследование классов следующим образом:

class LoginCredentials {
    password: string;
    email: string;
    constructor() {
        this.password = '';
        this.email = '';
    }
}

class RegisterCredentials extends LoginCredentials {
    username: string;
    constructor() {
        super();
        this.username = '';
    }
}

Машинопись площадка ссылка

Но, на мой взгляд, необходимость изменить способ кодирования только для кодирования типов, похоже, не соответствует моему пониманию философии TypeScript, заключающейся в том, что это просто надмножество JavaScript, в которое вы просто добавляете некоторую информацию для ввода, но вместо этого требуется, чтобы вы изменили способ вы кодируете JavaScript. Использование классов также удваивает количество строк при переносе до ≤es5, особенно при использовании const enums в первом блоке кода по сравнению с методом наследования классов.


person koral    schedule 13.07.2020    source источник


Ответы (1)


Здесь многое происходит:

  • вы не хотите делать username необязательным, потому что это необходимо, когда T равно TwoChoices.REGISTER.
  • правильный синтаксис для функции защиты типа - использовать is, чтобы утверждать, что переменная относится к проверенному типу. Здесь вы не используете универсальный тип, поскольку возвращаемый тип использует имя переменной, а не тип.
function isRegister(login_type: TwoChoices): login_type is TwoChoices.REGISTER {
    return login_type === TwoChoices.REGISTER;
}
  • ограничение T с помощью T extends TwoChoices не означает, что T может быть только одним из двух вариантов. Они оба действительны с точки зрения машинописного текста:
const myClass = new ConditionalGenericClass<TwoChoices.LOGIN | TwoChoices.REGISTER>(TwoChoices.LOGIN);
const myClass = new ConditionalGenericClass<TwoChoices>(TwoChoices.LOGIN);
  • проверка типа login_type в конструкторе сузит ваше понимание переменной login_type, но не сузит общий T класса - это в основном из-за предыдущего пункта маркера. Если T равно TwoChoices, а login_type равно TwoChoices.REGISTER, тогда username равно never, а не string, как вы ожидали, потому что он основан на T, а не на login_type.
  • ты прав, я считаю, что наследование лучше.

Возможное решение

Учитывая беспорядок текущей настройки, я думаю, вам придется утверждать, что у вас есть правильный тип для username с as, поскольку машинописный текст не сможет проверить это самостоятельно. Кажется, это работает:

type ConditionalUsername<T> = T extends TwoChoices.REGISTER ? string : never;

class ConditionalGenericClass<T extends TwoChoices> {
    password: string;
    email: string;
    action: T;
    username: ConditionalUsername<T>;
    constructor(login_type: T) {
        this.password = '';
        this.email = '';
        this.action = login_type;
        this.username = ((isRegister(login_type)) ? '' : undefined) as ConditionalUsername<T>;
    }
}

const loginClass = new ConditionalGenericClass(TwoChoices.LOGIN);
// expect type to be never
const loginU = loginClass.username;

const registerClass = new ConditionalGenericClass(TwoChoices.REGISTER);
// expect type to be string
const registerU = registerClass.username;

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

person Linda Paiste    schedule 30.09.2020