typescript TS2345即使在不可能的路径上

jvlzgdj9  于 2022-12-19  发布在  TypeScript
关注(0)|答案(1)|浏览(125)
export interface MyDoc{
  field: string;
}

function insert(doc: MyDoc) { /* db insert code here */ }

let x; // simulate undefined value
let doc = { field: x };

console.log(JSON.stringify(doc));
if (x === undefined) { // also does not prevent with doc.field === undefined
  return; // return if x is undefined
}

insert(doc); // causes TS2345: Argument of type error

就我而言,这是不可能的,但感觉像代码气味。我如何通知TS类型检查器doc不能包含值为undefinedfield,即使它可能用于其他目的(如日志记录)

3qpi33ja

3qpi33ja1#

TypeScript表现出的几乎所有narrowing行为都只作用于被检查的值。也就是说,对表达式exp1的某些操作可能会导致exp1的表观类型更改,但它几乎永远不会更改其他表达式exp2的表观类型。
有几个明显的例外:检查被鉴别的联合对象的鉴别属性将缩小对象本身的类型;有时候你可以将类型保护检查的结果保存为一个boolean值,然后稍后对该值的检查将缩小原始表达式的范围;并且您可以将有区别的联合分解为单独的变量,因此对一个变量的检查将缩小另一个变量的范围。
但是你所做的不是这些例外之一。语言中没有任何东西会导致对x的检查影响doc的表观类型。即使他们实现了microsoft/TypeScript#42384的建议,将属性类型保护扩展到非区分联合体的对象,它仍然不会给你这种行为。为了在总体上支持这类事情,编译器必须执行microsoft/TypeScript#46915中提到的"完全反事实分析",跟踪每个表达式的每一个可能的收缩,这对编译器来说是非常昂贵的,所以不太可能按照你想要的方式工作。
那么你能做些什么呢?
如果你不想重构你的代码,你可以使用一个类型Assert来告诉编译器它应该把doc当作MyDoc

if (x === undefined) throw new Error();
insert(doc as MyDoc); // assert

现在没有错误了,但是现在你要负责验证类型安全,因为编译器不能,如果你执行了错误的检查,仍然不会有错误:

if (x !== undefined) throw new Error(); // oops
insert(doc as MyDoc); // assert still "works"

所以要小心。
如果你对让编译器遵循你的逻辑更感兴趣,你将不得不重构成一个受支持的收缩方法。最灵活的方法是实现一个用户定义的类型保护函数,它可以准确地解释你对编译器所做的收缩。它仍然只对你所检查的值起作用,但至少你可以表达更复杂的类型。
例如,您可以编写一个检查,专门查看某个值是否为有效的MyDoc,如下所示:

function isMyDoc(x: any): x is MyDoc {
  return x && ("field" in x) && (typeof x.field === "string");
}

编译器不会验证该函数的实现,但现在您可以在其他地方随意使用它:

if (!isMyDoc(doc)) throw new Error();
insert(doc); // okay

或者,您可以编写一个更通用的保护,检查对象在给定键处是否具有定义的属性:

function hasDefinedProp<T extends object, K extends keyof T>(
  obj: T, key: K): obj is T & { [P in K]-?: Exclude<T[K], undefined> } {
  return typeof obj[key] !== "undefined"
}

这种输入有点复杂,但让我们看看它的实际操作:

if (!hasDefinedProp(doc, "field")) throw new Error();
doc;
// { field: string | undefined } & { field: string }
insert(doc); // okay

这是因为doc已经从{ field: string | undefined }缩小到了可以赋值给MyDoc{ field: string | undefined } & { field: string}hasDefinedProp()类型保护函数比isMyDoc()有更多的潜在用例,所以根据在代码库中进行这种检查的频率,它可能更有用。
但是,如果你对干净/清晰的代码感兴趣,最好的方法是重构,这样你就可以在开始变量别名之前做检查:

const x = Math.random() < 0.5 ? "abc" : undefined;
if (x === undefined) throw new Error(); // do this first
let doc = { field: x }; // then this
insert(doc); // okay

这种类型的重构并不总是对每个用例都可行,但是编译器和读者都将更容易理解这种明显的类型保护流。
Playground代码链接

相关问题