Checked the optional key value object but still get 'xxx' is possibly 'undefined' in typescript

huangapple go评论76阅读模式
英文:

Checked the optional key value object but still get 'xxx' is possibly 'undefined' in typescript

问题

I am passing an object which may have an optional key value like: var1?: number; I used arrayEqual to make sure all keys exist in the object before using it.

However, I still get 'input.var1' is possibly 'undefined' in const var1 = input.var1 + 3.

How can I solve it? Or should I check it before passing it to foo() (e.g., use arrayEqual to check before passing to foo(), so I can remove all ? in the 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});
英文:

I am passing an object which may have an optional ket value like: var1?: number; I used arrayEqual to make sure all key exists in the object before using it.

However, I still get 'input.var1' is possibly 'undefined'. in const var1 = input.var1 + 3.

How can I solve it? Or should I check it before passing it to foo() (e.g. use arrayEqual to check before passing to foo(), so I can remove all ? in the 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

得分: 1

不要使用 arraysEqual,它无法在静态分析中使用,你可以简单地检查 input.var1input.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 });

采用这种方法,TypeScript 可以确保在访问时 input.var1 是定义好的。

另外,这不是检查数组相等的好方法。同时,避免使用双等号操作符 (==),应使用 === 替代。

英文:

Instead of using arraysEqual, which cannot be statically analyzed, you can simply check if input.var1 or input.var2 is undefined.

Try this instead:

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 });

With this approach, TypeScript can be sure that input.var1 is defined when you access it.

As a side note, this is not a good way to check for equality of arrays. Also, refrain from using the double equal operator (==). Use === instead.

答案2

得分: 1

TypeScript 不理解 `arraysEqual(Object.keys(input), ["var1", "var2"])` 对于 `input` 的表面类型有任何含义。TypeScript [缩小类型](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)能力仅限于一组相当小的习惯性编码技巧,需要在编译器中显式实现,而该检查并不在其中。一旦检查了 [`Object.keys(input)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys) 的结果,编译器就无能为力了,因为返回类型只是 `string[]`,与 `input` 无关。参见 https://stackoverflow.com/q/55012174/2887218 了解更多信息。
在像这样的情况下,你可以采取一种方法,编写自己的[自定义类型保护函数](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)。你可以实现该函数以执行所需的检查,并为该函数提供一个调用签名,以表达检查将如何影响函数的一个参数。这基本上是一种方法,让你在编译器无法自行解决时告诉它如何缩小类型。
由于你试图检查键数组中的所有值是否存在于对象中,我们可以将检查称为 `containsKeys`,并为其提供一个调用签名,如下所示:

```typescript
declare function containsKeys<T extends Partial<Record<K, any>>, K extends keyof T>(
    obj: T, ...keys: K[]
): obj is T & Required<Pick<T, K>>;

```typescript
这是一个[泛型](https://www.typescriptlang.org/docs/handbook/2/generics.html)函数,接受一个泛型类型 `T` 的对象 `obj` 和一个泛型类型 `K[]` 的(展开的)数组 `keys`,其中 `T` 和 `K` 都被[约束](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints),以便 `K` 是已知可能存在于 `T` 中的所有键。返回值是 `obj is T & Required<Pick<T, K>>`(同时使用了 [`Required`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype) 和 [`Pick`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) 实用类型),这意味着 `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">>
    // 等价于 { 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`
英文:

TypeScript doesn't understand that arraysEqual(Object.keys(input), [&quot;var1&quot;, &quot;var2&quot;]) has any implication for the apparent type of input. The narrowing abilities of TypeScript are confined to a fairly small set of idiomatic coding techniques that needed to be explicitly implemented in the compiler, and that check isn't among them. As soon as you check the results of Object.keys(input) it's too late for the compiler to do anything, because the return type is just string[] and has nothing to do with input. See https://stackoverflow.com/q/55012174/2887218 for why.


One approach you can take in situations like this is to write your own custom type guard function. You implement the function so it does the check you need, and give the function a call signature that expresses how the check will affect one of the parameters of the function. It's basically a way for you to tell the compiler how to narrow things, when it can't figure it out for itself.

Since you're trying to check whether all of the values in an array of keys are present in an object, we can call the check containsKeys, and give it a call signature like:

declare function containsKeys&lt;T extends Partial&lt;Record&lt;K, any&gt;&gt;, K extends keyof T&gt;(
    obj: T, ...keys: K[]
): obj is T &amp; Required&lt;Pick&lt;T, K&gt;&gt;;

That's a generic function which accepts an object obj of generic type T and a (spread) array keys of generic type K[], where T and K are constrained so that K are all keys which are known to (possibly) be in T. And the return value is obj is T &amp; Required&lt;Pick&lt;T, K&gt;&gt; (using both the Required and Pick utility types), meaning that a true return value implies that the type of obj can be narrowed from T to a version of T where all the properties with keys in K are known to be present.

It can be implemented however you like, although the compiler will just believe you that any boolean-returning code is acceptable. So we can take your code and just drop it in:

function containsKeys&lt;T extends Partial&lt;Record&lt;K, any&gt;&gt;, K extends keyof T&gt;(
    obj: T, ...keys: K[]): obj is T &amp; Required&lt;Pick&lt;T, K&gt;&gt; {
    return JSON.stringify(Object.keys(obj)) == JSON.stringify(keys);    
}

But that's not a good check. It relies on the order of the keys being the same in both cases, and that's not really something you can guarantee. Instead you should consider doing some order-independent check; perhaps:

function containsKeys&lt;T extends Partial&lt;Record&lt;K, any&gt;&gt;, K extends keyof T&gt;(
    obj: T, ...keys: K[]): obj is T &amp; Required&lt;Pick&lt;T, K&gt;&gt; {
    return keys.every(k =&gt; k in obj);
}

And now we can test it out:

function foo(input: Input): number {
    if (!containsKeys(input, &quot;var1&quot;, &quot;var2&quot;)) {
        return 0
    }
 
   // input: Input &amp; Required&lt;Pick&lt;Input, &quot;var1&quot; | &quot;var2&quot;&gt;&gt;
   // equivalent to { var1: number; var2: number; }

    const var1 = input.var1 + 3
    return var1
}

That works. After the initial block, input has been narrowed from Input to Input &amp; Required&lt;Pick&lt;Input, &quot;var1&quot; | &quot;var2&quot;&gt;&gt;, a complicated-looking type equivalent to {var1: number; var2: number}... that is, the same type as Input with the keys required instead of optional. So input.var1 is known to be a number and not number | undefined.

Playground link to code

huangapple
  • 本文由 发表于 2023年8月9日 12:57:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76864696-2.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定