检查了可选的键值对象,但在 TypeScript 中仍然得到 'xxx' 可能是 'undefined'
我正在传递一个可能包含可选键值的对象,例如:var1?: number;我使用了arrayEqual来确保在使用对象之前所有键都存在。
然而,我在 const var1=input.var1 + 3 中仍然得到 'input.var1' 可能是 'undefined'.
我该如何解决它?或者我是否应该在传递给 foo() 之前检查它(例如,在传递给 foo() 之前使用 arrayEqual 检查,这样我就可以移除 inputT 中的所有 ?)?
interface inputT {
var1?: number;
var2?: number;
}
function arraysEqual(a1: string[], a2: string[]) {
return JSON.stringify(a1) == JSON.stringify(a2);
}
function foo(input: inputT): number {
if (
!arraysEqual(Object.keys(input), ["var1", "var2"])){
return 0
}
const var1 = input.var1 + 3
return var1
}
foo({"var1":2})
答案#1
TypeScript 不会理解 arraysEqual(Object.keys(input), ["var1", "var2"]) 对 input 的表观类型有任何影响。TypeScript 的类型缩小能力仅限于一小部分需要在编译器中显式实现的惯用编码技术,而这类检查并不在其中。一旦你检查了 Object.keys(input) 的结果,对于编译器来说已经太晚了,因为返回类型只是 string[],与 input 无关。关于原因,请参见《Why doesn't Object.keys return a keyof type in TypeScript?》。
在这种情况下,你可以采取的一种方法是编写你自己的自定义类型保护函数。你实现这个函数,使它执行你需要的检查,并为函数提供一个调用签名,表达该检查将如何影响函数的某个参数。这基本上是让你告诉编译器如何进行类型缩小,当它无法自行确定时。
由于你正在尝试检查一个键数组中的所有值是否都存在于一个对象中,我们可以将这个检查称为 containsKeys,并给它一个如下的调用签名:
declare function containsKeys<T extends Partial<Record<K, any>>, K extends keyof T>(
obj: T, ...keys: K[]
): obj is T & Required<Pick<T, K>>;
这是一个泛型函数,它接受一个泛型类型为 T 的对象 obj 和一个泛型类型为 K[] 的(展开)数组 keys,其中 T 和 K 受到约束,使得 K 是所有可能在 T 中已知的键。而返回值是 obj is T & Required<Pick<T, K>>(使用了 Required 和 Pick 工具类型),这意味着返回 true 时,obj 的类型可以从 T 缩小为一个版本的 T,其中 K 中的所有属性都已知存在。
它可以按照你喜欢的方式实现,尽管编译器只会相信你,任何返回 boolean 的代码都是可以接受的。所以我们可以拿你的代码直接放进去:
function containsKeys<T extends Partial<Record<K, any>>, K extends keyof T>(
obj: T, ...keys: K[]): obj is T & Required<Pick<T, K>> {
return JSON.stringify(Object.keys(obj)) == JSON.stringify(keys);
}
但那不是一个好的检查。它依赖于两种情况下键的顺序相同,而这实际上是无法保证的。相反,你应该考虑做一些与顺序无关的检查;比如:
function containsKeys<T extends Partial<Record<K, any>>, K extends keyof T>(
obj: T, ...keys: K[]): obj is T & Required<Pick<T, K>> {
return keys.every(k => k in obj);
}
现在我们可以测试它了:
function foo(input: Input): number {
if (!containsKeys(input, "var1", "var2")) {
return 0
}
// input: Input & Required<Pick<Input, "var1" | "var2">>
// equivalent to { var1: number; var2: number; }
const var1 = input.var1 + 3
return var1
}
这可以。在最初的代码块之后,input 已经从 Input 收窄到 Input & Required<Pick<Input, "var1" | "var2">>,这是一个看起来很复杂的类型,相当于 {var1: number; var2: number}……也就是说,与 Input 类型相同,只是键变为必填而非可选。因此 input.var1 已知是 number 类型,而不是 number | undefined。
代码的 Playground 链接
答案#2
与其使用无法静态分析的 arraysEqual,你可以简单地检查 input.var1 或 input.var2 是否未定义。
改试试这个:
interface inputT {
var1?: number;
var2?: number;
}
function foo(input: inputT): number {
if (input.var1 === undefined || input.var2 === undefined) {
return 0;
}
const var1 = input.var1 + 3;
return var1;
}
foo({ var1: 2 });
通过这种方法,当你访问 input.var1 时,TypeScript 可以确定它已被定义。
顺便提一下,这不是检查数组是否相等的好方法。此外,请避免使用双等号运算符(==)。请改用 ===。
