【问题标题】:TypeScript infers never type but requires assignmentTypeScript 推断从不输入,但需要赋值
【发布时间】:2019-02-22 15:17:45
【问题描述】:

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

我尝试使用默认为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 类型,就应该能够“不存在”。 作为 ab 实验,我也尝试让它回退到未定义。如果我手动分配mp4Options: undefined,这会起作用,否则类型检查器仍然会抱怨缺少属性。我认为绝对不应该是这种情况,因为我们可以省略开箱即用的 undefined 属性(没有条件类型)。

是否有解决方法或不那么复杂的方法?还是我的代码中有错误?

【问题讨论】:

    标签: typescript type-inference conditional-types


    【解决方案1】:

    这不是对我的问题的直接回答,而是试图编写一个更具可读性的解决方案。 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 对实际问题的回答以及遵循“经典”扩展基类路线的动机。

    【讨论】:

      【解决方案2】:

      我认为为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
      

      【讨论】:

      • 谢谢!我确实考虑过使用共享基类解决方案,但在那里遇到了类似的问题(加上将其集成到现有代码中的一些问题)。然而,有了这些指针,我想我可以将我的项目的当前状态重构为可行的东西。
      • @JanHettenkofer 如果您对共享基础有疑问,我们会回答 ;-)
      • 问题是我在基类中有options 属性以及它的构造函数。重写类根本没有改变构造函数,只改变了属性的类型。我现在很清楚为什么这不起作用(继承构造函数仍然需要一个unknown 选项成员),我想我之前没有足够的茶;-)。我改变了一些东西,我认为这将更具可读性(正如你正确指出的那样,魔法更少)。清理后,我将在明天添加共享基类方法作为答案。
      猜你喜欢
      • 2019-06-29
      • 2021-04-25
      • 2022-10-13
      • 2013-09-22
      • 2017-03-08
      • 1970-01-01
      • 2021-07-26
      • 2020-05-19
      相关资源
      最近更新 更多