かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript 定数を値に持つ配列で array.includes(value) しようとしたら Type error になる件

以前 Union 型に含まれるか判定するのに 配列を array.some で回して調べる方法を描いていました

array.includes() だと Type error になるという情報を見かけたので調べてみたのメモ

環境

TypeScript v4.9.5 (TypeScript Playground)

array.includes() だと Type error になる!

const arr = ['Ichigo', 'Aoi', 'Ran'] as const;
// const arr: readonly ["Ichigo", "Aoi", "Ran"]

type SoleilType = typeof arr[number];
// type SoleilType = "Ichigo" | "Aoi" | "Ran"

const isSoleil = (val: string): val is SoleilType => {
  return arr.includes(val);
  // Argument of type 'string' is not assignable to parameter of type '"Ichigo" | "Aoi" | "Ran"'.
};

const str = 'Yurika';
isSoleil(str);

arr.include(val) の箇所で Argument of type 'string' is not assignable to parameter of type '"Ichigo" | "Aoi" | "Ran"'. という型エラーが発生しました

配列の中身が定数なら同様にエラーになる

const ICHIGO = 'Ichigo' as const;
const AOI = 'Aoi' as const;
const RAN = 'Ran' as const;
const arr = [ICHIGO, AOI, RAN];
// const arr: ("Ichigo" | "Aoi" | "Ran")[]

type SoleilType = typeof arr[number];
// type SoleilType = "Ichigo" | "Aoi" | "Ran"

const isSoleil = (val: string): val is SoleilType => {
  return arr.includes(val);
  // Argument of type 'string' is not assignable to parameter of type '"Ichigo" | "Aoi" | "Ran"'.
};

解消方法 1. array.some() を使う

array.inclides を使わなくてもいいなら array.some() に置き換えてしまえば良い

type SoleilType = typeof arr[number];
// type SoleilType = "Ichigo" | "Aoi" | "Ran"

const isSoleil = (val: string): val is SoleilType => {
- return arr.includes(val);
+ return arr.some((v) => v === val);
};

const str = 'Yurika';
isSoleil(str); // => false

解消方法 2. array.includes() に渡す引数を any 型にキャストしてしまう

array.prototype.include は次のように定義されている

declare global {
  interface Array<T> {
    includes<U extends (T extends U ? unknown : never)>(
      searchElement: U, fromIndex?: number): boolean;
  }
}

Yes, technically it should be safe to allow the searchElement parameter in Array<T>.includes() to be a supertype of T, but the standard TypeScript library declaration assumes that it is just T. For most purposes, this is a good assumption, since you don't usually want to compare completely unrelated types as @GustavoLopes mentions. But your type isn't completely unrelated, is it?
cf. javascript - Why does the argument for Array.prototype.includes(searchElement) need the same type as array elements? - Stack Overflow

string[] のような配列に number の値が含まれるかなんて調べないよね?って想定で array.includes() の引数は配列に含まれる型 (Array<T>T) であることとされているらしい
なので as const された配列や含まれる要素が定数の場合は、配列に含まれる型 = 定数 になるので string 型などを渡そうとするとエラーになってしまうみたい

なのだけど、array.includes() に渡す引数を as any で any 型にしてしまうと配列に含まれる型として担保されないのだけどコンパイラが通る

type SoleilType = typeof arr[number];
// type SoleilType = "Ichigo" | "Aoi" | "Ran"

const isSoleil = (val: string): val is SoleilType => {
- return arr.includes(val);
+ return arr.includes(val as any);
};

const str = 'Yurika';
isSoleil(str); // => false

any 型は全ての型の親みたいなものなので、下記のような流れでコンパイラが通るのではないかと思う

includes<U extends (T extends U ? unknown : never)>
↓
includes<any extends (T extends any ? unknown : never)>
↓
includes<any extends unknown>

個人的な憶測だが any の場合は配列に含まれる型である可能性もあるので 100% 異なると言い切れないので受け付ける様になっているのではと思いました

まとめ

配列に含まれている値かチェックしたい時は

  1. array.some() を使う
  2. array.includes() を使う場合、定数の配列なら引数を as any にキャストして渡す

とおぼえておけば良さそうです
個人的には array.some() で良いんじゃないかな〜という印象

おわり ₍ ᐢ. ̫ .ᐢ ₎


[参考]