TypeScript никогда не печатает, но требует назначения - PullRequest
0 голосов
/ 18 сентября 2018

В моем проекте у меня есть класс, который действует как универсальный тип для файлов.В зависимости от того, какой тип файла мы обрабатываем, он должен предоставлять дополнительные свойства.

Я пытался добиться этого с помощью условных типов, которые по умолчанию имеют значение never, чтобы «скрыть» свойство.Однако, когда я пытаюсь использовать этот класс, средство проверки типов жалуется, что мне не хватает свойства, которое было выведено как тип never.Конечно, я не могу назначить его, поэтому у меня остался объект, который невозможно создать.

Ошибка происходит полностью внизу этого блока кода:

// just for convenience
type MP4OptionsT = {
    codec?: 'h264',
    profile: 'baseline' | 'main' | 'high',
    bitrate: number,
};

// this is the class in question
class MediaFile<Format extends 'mp4' | 'png'> {
    public path: string;
    public format: Format extends 'mp4' ? 'mp4' : Format extends 'png' ? 'png' : never;    // once the generic type argument is set, this can only be a specific string literal

    // this should not have to be assigned if generic type argument is 'png'
    public mp4Options: Format extends 'mp4' ? MP4OptionsT : never;

    constructor(opts: {
        path: string,
        format: Format extends 'mp4' ? 'mp4' : Format extends 'png' ? 'png' : never;
        // this should not have to be assigned if generic type argument is 'png' - however it demands to be assigned
        mp4Options: Format extends 'mp4' ? MP4OptionsT : never,
    }) {
        this.path = opts.path;
        this.format = opts.format;
        this.mp4Options = opts.mp4Options;
    }
}

// this is OK
const mp4File = new MediaFile<'mp4'>({
    path: '/some/file/somewhere.mp4',
    format: 'mp4',
    mp4Options: {
        profile: 'high',
        bitrate: 1000,
    }
});

// the type checker complains about this: "Property mp4Otions is missing in type {...}".
// if I explicitly include mp4Options, the type checker notes that "Type any is not assignable to Type never" - which makes sense, but precludes this class from ever being instantiated.
const pngFile = new MediaFile<'png'>({
    path: '/some/file/somewhere.png',
    format: 'png',    // since there is exactly one option for this, it would be nice if it were implicitly set...
});

Из моего понимания раздела Условные типы этой страницы http://www.typescriptlang.org/docs/handbook/advanced-types.html кажется, что mp4Options просто должен быть в состоянии "не там", если он был оценен как тип never.В качестве эксперимента я также попытался вернуть его к неопределенному.Это сработало, если я вручную назначил mp4Options: undefined, в противном случае средство проверки типов по-прежнему жаловалось на отсутствие свойств.Я думаю, что это определенно не должно иметь место, поскольку мы можем опустить свойства, которые undefined из коробки (без условного типа).

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

Ответы [ 2 ]

0 голосов
/ 19 сентября 2018

Это не прямой ответ на мой вопрос, а попытка написать более читаемое решение.Тициан Черникова-Драгомир уже предоставил очень хороший пример того, как сделать то, что я первоначально просил.

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

type LegalFormatT = 'mp4' | 'png' | 'jpg';

type FormatOptions<F extends LegalFormatT> = F extends 'mp4' ? { options: MP4OptionsT } : F extends 'png' ? { options: PNGOptionsT } : { options?: never };

type MP4OptionsT = {
    codec?: 'h264',
    profile: 'baseline' | 'main' | 'high',
    bitrate: number,
};

type PNGOptionsT = {
    sequence: boolean,
};

class MediaFile<Format extends LegalFormatT> {
    public path: string;
    public format: Format;

    constructor(opts: {
        path: string,
        format: Format,
    }) {
        this.path = opts.path;
        this.format = opts.format;
    }
}

class MP4MediaFile extends MediaFile<'mp4'> {
    public options: FormatOptions<'mp4'>['options'];

    constructor(opts: {
        path: string,
        options: MP4OptionsT,
    }) {
        super({
            path: opts.path,
            format: 'mp4',
        });
        this.options = opts.options;
    }
}

class PNGMediaFile extends MediaFile<'png'> {
    public options: FormatOptions<'png'>['options'];

    constructor(opts: {
        path: string,
        options: PNGOptionsT,
    }) {
        super({
            path: opts.path,
            format: 'png',
        });
        this.options = opts.options;
    }
}

class JPGMediaFile extends MediaFile<'jpg'> {
    public options: FormatOptions<'jpg'>['options'];

    constructor(opts: {
        path: string,
    }) {
        super({
            path: opts.path,
            format: 'jpg',
        });
    }
}

Хотя я действительно люблю использовать все функции вывода типов, которые предлагает TypeScript, я думаю, что в этом случае предпочтительнее «убить моего дорогого» и сделать несколькобольше ручной работы, чтобы не вызывать панику у будущего сопровождающего.

Большое спасибо Тициану Черниковой-Драгомиру за ответ на актуальный вопрос и мотивацию следовать «классическому» маршруту расширенного базового класса.

0 голосов
/ 18 сентября 2018

Я думаю, что вы могли бы лучше использовать общий базовый класс для MediaFile и получить два отдельных класса для форматов mp4 и png.

Если вы хотите перейти внизединственный класс с условным магическим маршрутом, мы можем это сделать.Хотя условные типы не могут влиять на необязательность свойства, как вы хотите, мы можем комбинировать их с типами пересечений, чтобы получить желаемый эффект:

// just for convenience
type MP4OptionsT = {
    codec?: 'h264',
    profile: 'baseline' | 'main' | 'high',
    bitrate: number,
};
type FormatOptions<F extends 'mp4' | 'png'> = (F extends 'mp4' ? { mp4Options: MP4OptionsT } : { mp4Options?: never})

class MediaFile<Format extends 'mp4' | 'png'> {
    public path: string;
    public format: Format // no need for a conditional type here, it the same type as Format

    public mp4Options: FormatOptions<Format>['mp4Options'];

    constructor(opts: {
        path: string,
        format: Format,
    } &  FormatOptions<Format>)
    {
        this.path = opts.path;
        this.format = opts.format;
        this.mp4Options = opts.mp4Options;
    }
}

// this is OK, no need for explicit type arguments
const mp4File = new MediaFile({
    path: '/some/file/somewhere.mp4',
    format: 'mp4',
    mp4Options: {
        profile: 'high',
        bitrate: 1000,
    }
});
mp4File.mp4Options.bitrate // ok 

// no need for the type argument 
const pngFile = new MediaFile({
    path: '/some/file/somewhere.png',
    format: 'png', // no need for mp4Options
});
pngFile.mp4Options.codec // error
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...