为什么 Array.prototype.includes(searchElement) 的参数需要与数组元素相同的类型?

问题描述 投票:0回答:3

老实说,我不知道我的设置是否有问题,或者这是否是打字稿功能。在以下示例中:

type AllowedChars = 'x' | 'y' | 'z';
const exampleArr: AllowedChars[] = ['x', 'y', 'z'];

function checkKey(e: KeyboardEvent) { 
    if (exampleArr.includes(e.key)) { // <-- here
        // ...
    } 
}

打字稿编译器抱怨

Argument of type 'string' is not assignable to parameter of type 'AllowedChars'.
但是我在哪里分配?
Array.prototype.includes
返回一个布尔值(我没有存储)。我可以通过类型断言来消除错误,如下所示:

if (exampleArr.includes(e.key as AllowedChars)) {}

但这怎么是正确的,我期待用户输入,它可以是任何东西。我不明白为什么用于检查

if
在数组中找到元素的函数(Array.prototype.includes())应该了解期望的输入类型。

我的

tsconfig.json
(打字稿v3.1.3):

 {
    "compilerOptions": {
      "target": "esnext",
      "moduleResolution": "node",
      "allowJs": true,
      "noEmit": true,
      "strict": true,
      "isolatedModules": true,
      "esModuleInterop": true,
      "jsx": "preserve",
    },
    "include": [
      "src"
    ],
    "exclude": [
      "node_modules",
      "**/__tests__/**"
    ]
  }

如有任何帮助,我们将不胜感激!

javascript typescript
3个回答
35
投票

有关 Array.prototype.includes() 和超类型的完整讨论,请参阅

microsoft/TypeScript#26255


是的,从技术上讲,允许

searchElement
中的
Array<T>.includes()
参数成为
T
的超类型应该是安全的,但 标准 TypeScript 库声明 假设它只是
T
。对于大多数目的来说,这是一个很好的假设,因为您通常不想像 @GustavoLopes 提到的那样比较完全不相关的类型。但你的类型并非完全无关,不是吗?

有不同的方法来处理这个问题。您所做的断言可能是最不正确的断言,因为您断言

string
AllowedChars
,即使它可能不是。它“完成了工作”,但你对此感到不安是对的。


另一种方法是通过 声明合并 本地覆盖标准库以接受超类型,这有点复杂,因为 TypeScript 不支持超类型约束(有关功能请求,请参阅 ms/TS#14520)。相反,声明使用条件类型来模拟超类型约束:

// remove "declare global" if you are writing your code in global scope to begin with
declare global {
  interface Array<T> {
    includes<U extends (T extends U ? unknown : never)>(
      searchElement: U, fromIndex?: number): boolean;
  }
}

然后你的原始代码就可以工作了:

if (exampleArr.includes(e.key)) {} // okay
// call to includes inspects as
// (method) Array<AllowedChars>.includes<string>(
//    searchElement: string, fromIndex?: number | undefined): boolean (+1 overload)

同时仍然防止完全不相关的类型的比较:

if (exampleArr.includes(123)) {} // error
// Argument of type '123' is not assignable to parameter of type 'AllowedChars'.

但是处理这个问题最简单且仍然正确的方法是将

exampleArr
的类型扩大为
readonly string[]
:

const stringArr: readonly string[] = exampleArr; // no assertion
if (stringArr.includes(e.key)) {}  // okay

或更简洁地说:

if ((exampleArr as readonly string[]).includes(e.key)) {} // okay

扩大到

readonly string[]
没问题,但要小心扩大到
string[]
,这有点危险,因为为了方便起见,TypeScript 不安全地将
Array<T>
视为 T 中的
covariant
。这对于阅读来说很好,但是当你编写属性时,你会遇到问题:

(exampleArr as string[]).push("oops"); // actually writing to an AllowedChars[]

但是由于您只是从数组中读取,所以它是完全安全的,这就是为什么建议使用

readonly


Playground 代码链接


2
投票

如果你比较两种不同类型,那么它们自然是不同的。

想象你有:

type A = {paramA: string};
type B = {paramB: number};

const valuesA: A[] = [{paramA: 'whatever'}];
const valueB: B = {paramB: 5};

valuesA.includes(valueB); // This will always be false, so it does not even make sense

在您的情况下,编译器将

AllowedChars
视为与
string
完全不同的类型。您必须将收到的
string
“投射”到
AllowedChars

但这如何正确,我期待用户输入,它可以是任何东西。

编译器不知道你想用

includes
来完成什么。它只知道它们有不同的类型,因此不应该比较它们。


0
投票

最终只使用

.some
而不是
.includes
就帮了我的忙。这里是游乐场

const FRUITS = ["apple", "banana", "orange"] as const
type Fruit = typeof FRUITS[number]

const isFruit = (value: string): value is Fruit =>
    FRUITS.some(fruit => fruit === value)
© www.soinside.com 2019 - 2024. All rights reserved.