Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to solve TypeScript being too restrictive on Array.includes()

Tags:

typescript

I should be allowed to pass anything into Array.includes to check if it's in the array, but TypeScript doesn't want me to pass in something that isn't the right type. For instance:

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'.
}

Playground

What is a clean way to fix this code with minimal impact on readability?

like image 384
ZYinMD Avatar asked Nov 23 '25 04:11

ZYinMD


1 Answers

First, please read this QA from TypeScript 3.x days where someone was asking essentially the same question as yourself: TypeScript const assertions: how to use Array.prototype.includes?

Now, in your case, as an alternative to @CertainPerformance's suggestion of unknown[] (which loses type information), you can legally widen fruits to readonly string[] (without using as), which is compatible with Fruit and 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);
}

An alternative, (theoretically more "correct") approach is to add a variant includes member to the ReadonlyArray<T> interface (as suggested in the linked QA) which allows U to be a supertype of T instead of the other way around.

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);
}


Having said all of that... if you intend to use a collection-type as a value/type set-membership test, you should use a JavaScript Set<T> (or use object keys) instead of an Array: not only because of performance reasons (as both Set<T>.has() and object key lookup are O(1) but Array.includes() is O(n) - and also because TypeScript works better with keyof types.

...implementing that is an exercise for the reader.

like image 121
Dai Avatar answered Nov 28 '25 18:11

Dai