[英]How to avoid the double non-nullable check in TypeScript discriminated unions?
在下面的代码 TypeScript 不相信如果customMessage
是undefined
, className
不是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
})
上面的例子可以工作,但是:
else if (typeof parametersObject.className !== "undefined")
customMessage
或className
之一已被初始化。 这是关键时的示例: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 相信如果customMessage
是undefined
, className
不是 undefined ,反之亦然,而不是如何初始化this.message
( ClassRequiredInitializationHasNotBeenExecutedError
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.