如何解决typescript对Array.includes()的限制



我应该被允许传递任何东西到Array.includes来检查它是否在数组中,但是typescript不希望我传递不正确类型的东西。例如:

游乐场

type Fruit = "apple" | "orange";
type Food = Fruit | "potato";
const fruits: Fruit[] = ["apple", "orange"];
function isFruit(thing: Food) {
return fruits.includes(thing); // ts error: "potato" is not assignable to type 'Fruit'.
}

在对可读性影响最小的情况下修复这些代码的干净方法是什么?

首先,请阅读这篇来自TypeScript 3的QA。x天,有人问了一个和你一样的问题:TypeScript const断言:如何使用Array.prototype.includes?

现在,在您的情况下,作为@某些性能建议的unknown[](丢失类型信息)的替代方案,您可以合法地将fruits扩展到readonly string[](不使用as),这与FruitFood兼容:

type Fruit = "apple" | "orange";
type Food = Fruit | "potato" | "egg";
const fruits: readonly Fruit[] = ["apple", "orange"];
function isFruit(food: Food): food is Fruit {
const fruitsAsStrings: readonly string[] = fruits;
return fruitsAsStrings.includes(food);
}

另一种方法(理论上更"正确")是向ReadonlyArray<T>接口添加变体includes成员(如链接的QA中所建议的),这允许U成为T的超类型,而不是其他方式。

interface ReadonlyArray<T> {
includes<U>(x: U & ((T & U) extends never ? never : unknown)): boolean;
}
type Fruit = "apple" | "orange";
type Food = Fruit | "potato" | "egg";
const fruits: readonly Fruit[] = ["apple", "orange"];
function isFruit(food: Food): food is Fruit {
return fruits.includes(food);
}

说了这么多…如果你打算使用集合类型作为值/类型集成员测试,你应该使用JavaScriptSet<T>(或使用object键)而不是Array:不仅是因为性能原因(因为Set<T>.has()object键查找都是O(1),但Array.includes()O(n)),还因为TypeScript在keyof类型下工作得更好。

…实现这个是读者的练习。

我最终这样做了(似乎没有缺点):

function isFruit(thing: Food) {
return (fruits as Food[]).includes(thing);
}

你最后的解决方案(type-cast)对我来说似乎很好。

你遇到的问题并不是TypeScript独有的。如果我们看一下Java的List.contains方法,我们会看到,即使你可能有一个字符串列表,contains方法接受任何参数类型(对象)。TypeScript选择了一种更严格的方法,将includes限制为只有那些在数组中的类型。

在我看来,Java的选择过于自由,而TypeScript的选择过于限制。但这些都是解决问题最简单的方法。毕竟,如果您选择了介于两者之间的类型,则必须以某种方式能够显示传递给includes的类型与数组中的类型相关。

我选择接受这个挑战,并在Rimbu不可变集合库中为这些类型的方法设置了相同的默认值:

import { HashSet } from "@rimbu/core";
type Fruit = "apple" | "orange";
type Food = Fruit | "potato";
type NonFruit = "car" | "house";
const fruits = HashSet.of<Fruit>("apple", "orange");
function isFruit(thing: Food) {
// return fruits.has(thing);           // Error: "potato" not assignable to "Fruit"
// return fruits.has(3);               // Error: 3 not assignable to "Fruit"
// return fruits.has<NonFruit>("car"); // Error: "car" not assignable to Fruit
return fruits.has<Food>(thing);        // Correct way
}

在示例中,您可以看到,仅仅说fruits.has(thing)就会导致编译器错误。毕竟,类型Food不是Fruit的子集,所以这可能是一个错误。但是,如果您通过提供Food类型参数告诉该方法这是有意的,那么一切都没问题。

现在,为什么fruit.has<NonFruit>("car")不起作用?has方法有一个限制,集合类型和参数应该在某种程度上"相关"。(具体来说,任何一个都必须扩展另一个)。类型FruitNonFruit不是这种情况。因此,即使提供类型仍然会导致编译器错误。如果您确实想要这样做,您当然可以始终提供any作为参数。

虽然这种方法仍然不完美,但我认为它涵盖了此类方法95%的用例。因此,你会发现这种方法在Rimbu集合中无处不在。

这里是一个CodeSandbox,你可以在这里玩这个例子。

最新更新