繁体   English   中英

如何避免 TypeScript 可区分联合中的双重不可空检查?

[英]How to avoid the double non-nullable check in TypeScript discriminated unions?

在下面的代码 TypeScript 不相信如果customMessageundefinedclassName不是undefined

export default class ClassRequiredInitializationHasNotBeenExecutedError extends Error {

  public static readonly NAME: string = "ClassRequiredInitializationHasNotBeenExecutedError";

  public constructor(
      parametersObject: {
        customMessage: string;
        className?: undefined;
      } | {
        className: string;
        customMessage?: undefined;
      }
  ) {

    super();

    this.name = ClassRequiredInitializationHasNotBeenExecutedError.NAME;

    if (typeof parametersObject.customMessage !== "undefined") {
      this.message = parametersObject.customMessage;
    } else {
      this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
          className: parametersObject.className
      })
    }
  }

  private static buildMessage(parametersObject: { className: string }): string {
    return `The class '${parametersObject.className}' has not been executed;`
  }
}

小提琴

错误:

Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.(2322)

我知道如果会进行仔细检查,例如:

if (typeof parametersObject.customMessage !== "undefined") {
    this.message = parametersObject.customMessage;
} else if (typeof parametersObject.className !== "undefined") {
    this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
        className: parametersObject.className
})

上面的例子可以工作,但是:

  1. 如果可能的话,我想避免else if (typeof parametersObject.className !== "undefined")
  2. 事件离开它,TypeScript 不会相信customMessageclassName之一已被初始化。 这是关键时的示例:
let message: string;

if (typeof parametersObject.customMessage !== "undefined") {
    message = parametersObject.customMessage;
} else if (typeof parametersObject.className !== "undefined") {
    message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
        className: parametersObject.className
    })
}

console.log(message.length)
Variable 'message' is used before being assigned.(2454)

请注意,这个问题是关于如何让 TypeScript 相信如果customMessageundefinedclassName不是 undefined ,反之亦然,而不是如何初始化this.messageClassRequiredInitializationHasNotBeenExecutedError class 只是示例)

 parametersObject: {
        customMessage: string;
        className?: undefined;
      } | {
        className: string;
        customMessage?: undefined;
      }

可以简化为:

parametersObject: {
        customMessage: string;
      } | {
        className: string;
      }

as property?: undefined几乎没有带来额外的信息。

那么条件可以表示为:

if ('customMessage' in parametersObject) {
  this.message = parametersObject.customMessage;
} else {
  this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
      className: parametersObject.className
  })
}

操场

更新

严格强制一个或另一个选项:

type Params<T extends {
        customMessage: string;
      } | {
        className: string;
      }>  = T extends {
        customMessage: any;
        className: any;
      }  ? never : T

export default class ClassRequiredInitializationHasNotBeenExecutedError<P extends {
        customMessage: string;
      } | {
        className: string;
      }> extends Error {

  public static readonly NAME: string = "ClassRequiredInitializationHasNotBeenExecutedError";

  public constructor(
      parametersObject: P extends Params<P> ? P : never
  ) {

    super();

    this.name = ClassRequiredInitializationHasNotBeenExecutedError.NAME;

    if ('customMessage' in parametersObject) {
      this.message = parametersObject.customMessage;
    } else {
      this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
          className: parametersObject.className
      })
    }
  }

  private static buildMessage(parametersObject: { className: string }): string {
    return `The class '${parametersObject.className}' has not been executed;`
  }
}

new ClassRequiredInitializationHasNotBeenExecutedError({ customMessage: ''  }) // ok
new ClassRequiredInitializationHasNotBeenExecutedError({ className: ''  }) // ok
new ClassRequiredInitializationHasNotBeenExecutedError({ customMessage: '', className: ''  }) // error as expected

操场

typeof parametersObject.customMessage仅区分parametersObject.customMessage的类型,但不区分parametersObject - 当您将其视为 function 时,这有点道理: typeof(parametersObject.customMessage)

当您检查以下示例 ( Playground ) 中的推断类型时,您可以更好地可视化这一点:

function foo(
  param: {
    val1: string;
    val2: number;
  } | {
    val1: number;
    val2: string;
  }
) {
  if (typeof param.val1 === "number") {
    // param.val1 is discriminated to type number
    // BUT: param is *still* type
    //  {
    //    val1: string; val2: number
    //  } | {
    //    val1: number; val2: string
    //  }
    //
    // so param.val2 is inferred to type `string | number`
    param.val2;
    return param.val1;
  } else {
    // param.val1: string
    // param.val2: string | number
    param.val2;
    return param.val1;
  }
}

虽然在if分支 TypeScript 可以区分param.val1是一个number ,但它不会推断param{val1: number; val2: string;} {val1: number; val2: string;} else分支正好相反: param.val1被正确推断为string ,但param仍被推断为{val1: number; val2: string;} {val1: number; val2: string;}

因此,在这两种情况下param.val2都无法缩小范围,并被推断为string | number string | number


您可以使用相等比较parametersObject.customMessage !== undefined而不是typeof ,它也可以正确推断parametersObject的类型( Playground ):

if (parametersObject.customMessage !== undefined) {
  this.message = parametersObject.customMessage;
} else {
  this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
      className: parametersObject.className
  })
}

如果传递给您的构造函数的对象实际上不包含可选参数,那么遵循artur grzesiak 的答案可能会更好:从类型中删除可选键并在条件( Playground )中使用in运算符

  public constructor(
      parametersObject: {
        customMessage: string;
      } | {
        className: string;
      }
  ) {

    super();

    this.name = ClassRequiredInitializationHasNotBeenExecutedError.NAME;

    if ('customMessage' in parametersObject) {
      this.message = parametersObject.customMessage;
    } else {
      this.message = ClassRequiredInitializationHasNotBeenExecutedError.buildMessage({
          className: parametersObject.className
      })
    }
  }

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM