英文:
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
,对于{}
、[]
、null
、undefined
等等,它将返回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[]) => {
if (!isNull(arrayOfObjects)) {
// do something here, e.g.:
arrayOfObjects?.map((c) => ...); // line 4
}
}
Of course, Typescript still thinks that the type of `arrayOfObjects` in line 4 is still `any[] | undefined`, even though it'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'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[]) => {
if (!isNullOrEmpty(arrayOfObjects)) {
arrayOfObjects?.map((c) => ...); // Property 'map' does not exist on type 'never'
}
}
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) => {...}
// ^
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<string, never> | [] | null | undefined) => {
return true // implementation TBD
}
const customFunction = (arrayOfObjects?: any[]) => {
if (!isNullOrEmpty(arrayOfObjects)) {
arrayOfObjects?.map((c) => c); // fine
}
}
英文:
I think the problem is here.
export const isNullOrEmpty = (value: any): value is ({} | [] | null | undefined) => {...}
// ^
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<string, never> | [] | null | undefined) => {
return true // implementation TBD
}
const customFunction = (arrayOfObjects?: any[]) => {
if (!isNullOrEmpty(arrayOfObjects)) {
arrayOfObjects?.map((c) => c); // fine
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论