TypeScript推断从不键入但需要分配

时间:2018-09-18 14:38:33

标签: typescript type-inference conditional-types

在我的项目中,我有一个类充当文件的通用类型。根据我们正在处理的文件类型,它应该显示其他属性。

我尝试使用默认为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 :(得分:1)

我认为为MediaFile使用一个公共基类并为mp4png格式派生两个单独的类可能会更好。

如果您确实想通过条件魔术路线去上一堂课,我们可以做到。尽管条件类型不会像您想要的那样影响属性的可选性,但是我们可以将它们与交集类型结合使用以获得预期的效果:

// 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

答案 1 :(得分:0)

这不是我的问题的直接答案,而是尝试编写更具可读性的解决方案。提香·切尔尼科娃·德拉戈米尔(Titian Cernicova-Dragomir)已经提供了一个很好的例子,说明了如何按照我最初的要求去做。

经过更多的研究后,我想到了这个解决方案,它避免了我在原始问题中提出的复杂类型推断:

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提供的所有类型推断功能,但我认为在这种情况下,最好“杀死我的宝贝”,并做一些更多的手工工作,以免引起未来的维护者恐慌。

非常感谢Titian Cernicova-Dragomir回答了遵循“经典”扩展基类路线的实际问题和动机。