我应该被允许传递任何东西到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
),这与Fruit
和Food
兼容:
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
方法有一个限制,集合类型和参数应该在某种程度上"相关"。(具体来说,任何一个都必须扩展另一个)。类型Fruit
和NonFruit
不是这种情况。因此,即使提供类型仍然会导致编译器错误。如果您确实想要这样做,您当然可以始终提供any
作为参数。
虽然这种方法仍然不完美,但我认为它涵盖了此类方法95%的用例。因此,你会发现这种方法在Rimbu集合中无处不在。
这里是一个CodeSandbox,你可以在这里玩这个例子。