🌀

【沼】never に意味を持たせない

に公開

はじめに

こんにちは。最近型レベルプログラミングにハマりつつある yossuli です。
本記事は僕が勝手に行っている #1 日 1Zenn 7 日目の記事になります
今回も型を捏ねていて困ったことについて、具体的に何が起きたのかとその解消法について書いていきます。

本題

export type FindIndex<T extends any[], U, Acc extends any[] = []> = T extends [
  infer F,
  ...infer R
]
  ? F extends U
    ? Acc["length"]
    : FindIndex<R, U, [...Acc, F]>
  : never;

export type FindIndexTuple<
  T extends any[][],
  U,
  Acc extends any[][] = []
> = T extends [infer F extends any[], ...infer R extends any[][]]
  ? FindIndex<F, U> extends infer Index
    ? Index extends never
      ? FindIndexTuple<R, U, [...Acc, F]>
      : [Acc["length"], Index]
    : never
  : never;

この FindIndexTuple は、2 次元配列の中から特定の要素を探し、その要素が見つかった場合はその要素のインデックスのタプルを返す型です。
2 次元配列から特定の要素を探すために、まずは 1 次元配列の中から特定の要素を探す FindIndex を作成し、それを再帰的に使用して 2 次元配列の中から特定の要素を探しています。
しかし、ここで問題が発生しました。

type Test = FindIndexTuple<[[1, 2], [3, 4]], 3>;
//   ^ expected [1, 0]
//     actual never

しかしよくよく考えてみると never というのは「何もない」という意味であるため、値が見つからなかったという意味合いを持たせるのはおかしなことです。

Never

The never type represents the type of values that never occur.

never は、決して発生しない値の型を表します。

^1

typescript のドキュメントにもあるように、never があらわすのは「決して発生しない値の型」であり、何かしら他の情報を持たせてはいけません。
エラーのような場合にのみ never が返るようにするべきであり、通常 never が返るように設計しないべきです。
配列メソッドの findIndex のように、値が存在しない場合は -1 を返す方が適切です。

そこで、never を返すのではなく、-1 を返すように型を修正しました。

export type FindIndex<T extends any[], U, Acc extends any[] = []> = T extends [
  infer F,
  ...infer R
]
  ? F extends U
    ? Acc["length"]
    : FindIndex<R, U, [...Acc, F]>
  : -1;
export type FindIndexTuple<
  T extends any[][],
  U,
  Acc extends any[][] = []
> = T extends [infer F extends any[], ...infer R extends any[][]]
  ? FindIndex<F, U> extends infer Index
    ? Index extends -1
      ? FindIndexTuple<R, U, [...Acc, F]>
      : [Acc["length"], Index]
    : never
  : never;

このようにしたところ思い通りに型が推論されるようになりました。

type Test = FindIndexTuple<[[1, 2], [3, 4]], 3>;
//   ^ [1, 0]

まとめ

雰囲気でコーディングせず、きちんと構文の意味を理解することが大切ですね...

Discussion