Type guards for exportable utility functions: isNull, isNullOrWhitespace, etc

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

Type guards for exportable utility functions: isNull, isNullOrWhitespace, etc

问题

以下是您要翻译的内容的部分:

我有一个使用TypeScript的React项目其中我经常使用一些实用函数`isFunction``isNull``isNullOrEmpty``isNullOrWhitespace`等等

我可以在我的实用文件中有这样的东西

export const isNull = (value: any): boolean => value === null || value === undefined;

然后我会在其他地方像这样使用它(非常牵强的示例):

const customFunction(arrayOfObjects?: any[]) => {
    if (!isNull(arrayOfObjects)) {
        // 在这里做一些事情,例如:
        arrayOfObjects?.map((c) => ...); // 行4
    }
}

当然,TypeScript仍然认为行4中arrayOfObjects的类型仍然是any[] | undefined,尽管它进入了这个块中肯定不是null或undefined。所以我找到了处理这个问题的正确方法是使用类型守卫。如果我改变我的实用函数如下:

export const isNull = (value: any): value is null | undefined => value === null || value === undefined;

然后isNull检查就能按预期工作,正确地检测到行4中的arrayOfObjects不是null或undefined。

然后我遇到的问题是在进行更高级函数的类型检查时。我不希望调用arrayOfObjects.map(),除非我知道arrayOfObjects不是null、undefined、对象或空数组。所以我想定义一个函数isNullOrEmpty,对于{}[]nullundefined等等,它将返回false,但将这些传递给类型守卫会导致:

const customFunction(arrayOfObjects?: any[]) => {
    if (!isNullOrEmpty(arrayOfObjects)) {
        arrayOfObjects?.map((c) => ...); // 类型'never'上不存在属性'map'
    }
}

在检查函数是否为函数时,我遇到类似的问题:

export const isFunction = (value: any): boolean => typeof value === 'function';

或者检查(可能相关的问题):

export const isNullOrWhitespace = (value: any): boolean => isNullOrEmpty(value) || (typeof (value) === 'string' && !value.trim());

特别是因为我经常检查某些东西是否不是null或空白,所以我不知道如何获得这些类型的否定。

正确导出可以进行类型检查的函数的方式是什么?


<details>
<summary>英文:</summary>

I have a React project with Typescript in which I frequently use some utility functions: `isFunction`, `isNull`, `isNullOrEmpty`, `isNullOrWhitespace`, and so on.

I can have something like this in my utility file: 

export const isNull = (value: any): boolean => value === null || value === undefined;


And then I will use it elsewhere like so (very contrived example):

    const customFunction(arrayOfObjects?: any[]) =&gt; {
        if (!isNull(arrayOfObjects)) {
            // do something here, e.g.:
            arrayOfObjects?.map((c) =&gt; ...); // line 4
        }
    }

Of course, Typescript still thinks that the type of `arrayOfObjects` in line 4 is still `any[] | undefined`, even though it&#39;s certainly not null or undefined if it made it inside this block. So I found out the correct way to handle this is using type guards. If I instead change my utility function like so: 

export const isNull = (value: any): value is null | undefined => value === null || value === undefined;


Then the `isNull` check works as expected, and correctly detects that `arrayOfObjects` on line 4 is not null or undefined.


The problem I am then having is type-checking more advanced functions. I wouldn&#39;t want to call `arrayOfObjects.map()` unless I knew that `arrayOfObjects` was not null, undefined, an object, or an empty array. So I want to define a function `isNullOrEmpty` that will return false for e.g. `{}, [], null, undefined`, but throwing these into the type guard 

export const isNullOrEmpty = (value: any): value is ({} | [] | null | undefined) => {...}


results in:

    const customFunction(arrayOfObjects?: any[]) =&gt; {
        if (!isNullOrEmpty(arrayOfObjects)) {
            arrayOfObjects?.map((c) =&gt; ...); // Property &#39;map&#39; does not exist on type &#39;never&#39;
        }
    }


I have similar issues when dealing with checking for a function:

export const isFunction = (value: any): boolean => typeof value === 'function';


Or checking that ([Potentially related issue][1]?): 

export const isNullOrWhitespace = (value: any): boolean => isNullOrEmpty(value) || (typeof (value) === 'string' && !value.trim());


Particularly because I am so often checking that something is *not* null or whitespace, so I am not sure how to grab the negation of those types as well.

What is the correct way to export functions that can type check?


  [1]: https://github.com/microsoft/TypeScript/issues/42101

</details>


# 答案1
**得分**: 1

I think the problem is here.

```javascript
export const isNullOrEmpty = (value: any): value is ({} | [] | null | undefined) =&gt; {...}
//                                                   ^

The {} is not an empty object. Object types actually mean that they require at least the keys they specify, but they could have more:

const objA = { a: 123, b: 456 }
const objB: { a: number } = objB // fine

Here objA has at least all the keys required by the type of objB, so the assignment is allowed.

Following that logic, the {} type is an object type that requires at zero keys. Which means almost anything can be assigned to it, since every value has at least zero keys.

const objA = { a: 123, b: 456 }
const testA: {} = objA // fine
const testB: {} = true // fine
const testC: {} = 456 // fine

ESLint even has a rule for this pitfall

> Avoid the Object and {} types, as they mean "any non-nullish value". This is a point of confusion for many developers, who think it means "any object type".
>
> See this comment for more information.


So when your type predicate function says that a value is {}, and you negate that, you are saying that your value is not assignable to {}, which as mentioned nearly everything is. So the result is never.


A better type for an empty object is:

Record<string, never>

This is an object type that has exactly zero keys. It's keys must be a subset of string, and the value of those keys must also match the type of never. And since never cannot actually exist at runtime, this means the object must be actually empty.

So all string keys must not have a value.

So replace {} with that record type, and your code works.

export const isNullOrEmpty = (value: any):
    value is (Record&lt;string, never&gt; | [] | null | undefined) =&gt; {
        return true // implementation TBD
    }

const customFunction = (arrayOfObjects?: any[]) =&gt; {
    if (!isNullOrEmpty(arrayOfObjects)) {
        arrayOfObjects?.map((c) =&gt; c); // fine
    }
}

See playground

英文:

I think the problem is here.

export const isNullOrEmpty = (value: any): value is ({} | [] | null | undefined) =&gt; {...}
//                                                   ^

The {} is not an empty object. Object types actually mean that they require at least the keys they specify, but they could have more:

const objA = { a: 123, b: 456 }
const objB: { a: number } = objB // fine

Here objA has at least all the keys required by the type of objB, so the assignment is allowed.

Following that logic, the {} type is an object type that requires at zero keys. Which means almost anything can be assigned to it, since every value has at least zero keys.

const objA = { a: 123, b: 456 }
const testA: {} = objA // fine
const testB: {} = true // fine
const testC: {} = 456 // fine

ESLint even has a rule for this pitfall

> Avoid the Object and {} types, as they mean "any non-nullish value". This is a point of confusion for many developers, who think it means "any object type".
>
> See this comment for more information.


So when your type predicate function says that a value is {}, and you negate that, you are saying that your value is not assignable to {}, which as mentioned nearly everything is. So the result is never.


A better type for an empty object is:

Record&lt;string, never&gt;

This is an object type that has exactly zero keys. It's keys must be a subset of string, and the value of those keys must also match the type of never. And since never cannot actually exist at runtime, this means the object must be actually empty.

So all string keys must not have a value.

So replace {} with that record type, and your code works.

export const isNullOrEmpty = (value: any):
    value is (Record&lt;string, never&gt; | [] | null | undefined) =&gt; {
        return true // implementation TBD
    }

const customFunction = (arrayOfObjects?: any[]) =&gt; {
    if (!isNullOrEmpty(arrayOfObjects)) {
        arrayOfObjects?.map((c) =&gt; c); // fine
    }
}

See playground

huangapple
  • 本文由 发表于 2023年2月14日 03:09:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/75440258.html
匿名

发表评论

匿名网友

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

确定