🟦

TypeScriptでFind型を作ってみた

2021/07/12に公開

作ったもの

以下のテストが通る Find

import { Equal, Expect } from '@type-challenges/utils'

type Lang = 'TypeScript'|'PHP'|'Swift'|'Python'

type cases = [
  // 配列側が確定していて、探す要素が1通りしかないとき
  // 探していた要素か undefined を明確に返す
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], 'TypeScript'>, 'TypeScript'>>,
  Expect<Equal<Find<readonly ['TypeScript', 'PHP', 'Swift'], 'TypeScript'>, 'TypeScript'>>,
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], 'Python'>, undefined>>,

  // 配列側が確定していて、探す要素が複数通りあるとき
  // 探している要素のうち一致するものか undefined を返す
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], 'Python'|'Swift'>, 'Swift'|undefined>>,
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], Lang>, 'TypeScript'|'PHP'|'Swift'|undefined>>,

  // 配列側が不確定で、探す要素が1通りしかないとき
  // 探している要素のうち一致するものか undefined を返す
  Expect<Equal<Find<Lang[], 'TypeScript'>, 'TypeScript'|undefined>>,
  Expect<Equal<Find<readonly Lang[], 'TypeScript'>, 'TypeScript'|undefined>>,
  // 一致しないものは undefined を返す
  Expect<Equal<Find<Lang[], 'Rust'>, undefined>>,

  // 配列側が不確定で探す要素も複数通りあるとき
  // 探している要素か undefined を返す
  Expect<Equal<Find<Lang[], 'PHP'|'Swift'>, 'PHP'|'Swift'|undefined>>,
  Expect<Equal<Find<Lang[], Lang>, Lang|undefined>>,
]

完成品は こちら で確認できます

1. 配列の取りうる要素の型を得る

まずは次のような 明確に型推論できるパターンを考えていきます

type cases = [
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], 'TypeScript'>, 'TypeScript'>>,
  Expect<Equal<Find<['TypeScript', 'PHP', 'Swift'], 'Python'>, undefined>>,
]

このテストを通すには 探す要素 が 配列の取りうる要素の型( ['foo', 'bar', 'baz'] なら 'foo'|'bar'|'baz' ) の中にあるかを判定します

配列から union型 に変えるには 任意の数値インデックスを指定してあげます

type ArrayToUnion<T extends any[]> = T[number]

// 'foo'|'bar'|'baz'
type UnionElements = ArrayToUnion<['foo', 'bar', 'baz']>

これを使って Find 型 ver0.1を作ります

Find ver0.1
type Find<Haystack extends any[], Needle extends any> =
  Needle extends Haystack[number] // NeedleがHaystack要素のいずれかか
    ? Needle
    : undefined

これでいくつかのテストが通るようになりました

// このようなケースは
type A = Find<['TypeScript', 'PHP', 'Swift'], 'Python'|'Swift'>
// ユニオン型で分割した この型と同じ
type B = Find<['TypeScript', 'PHP', 'Swift'], 'Python'> | Find<['TypeScript', 'PHP', 'Swift'], 'Swift'>

2. 要素数が確定しているか判定する

次は、要素数が確定していないケースを考えます

type cases = [
  Expect<Equal<Find<Lang[], 'TypeScript'>, 'TypeScript'|undefined>>,
]

配列の要素数が不確定の場合は、探している要素が存在しているか分からないため、Needle|undefined を返したいです
つまり、要素数が確定しているかどうかの判定が必要になります

要素数を取得するには length プロパティを使います

type Length<T extends any[]> = T['length']

// 要素数が確定している場合は具体的な数値
// 0
type A = Length<[]>
// 1
type B = Length<['TypeScript']>


// 要素数が不確定な場合は number
// number
type C = Length<Lang[]>

要素数が 0 の場合は [][number]never なので Needle extends Haystack[number] を満たさず、ver0.1 で、すでに undefined が返るようになっています
つまり 0 extends Haystack['length'] を満たすときは、要素数が具体的な数値ではなく number のときのみになります
これを利用して Find 型 ver0.2 を作ります

Find ver0.2
type Find<Haystack extends any[], Needle extends any> =
  Needle extends Haystack[number]
+   // ここには空配列が来ないので 0 を満たしている時は
+   // 1や2などの具体的な数値ではなく number ときのみ
+   ? 0 extends Haystack['length']
+     ? Needle|undefined
-   ? Needle
+     : Needle
    : undefined // 空配列はここで undefined が返される

3. readonly にしてあげる

最後に readonly がついているテストケースに対応させます

type cases = [
    Expect<Equal<Find<readonly ['TypeScript', 'PHP', 'Swift'], 'TypeScript'>, 'TypeScript'>>,
]

readonly がついているだけで使いどころが分かりにくいので具体的な使用例を考えてみます

// as constで受け取るための readonly
class Langs<T extends readonly Lang[]> {
  constructor(private langs: T) {}

  find<V extends T[number]>(element: V): Find<T, V> {
    return undefined as Find<T, V> // 実装は無視してください
  }
}

Langs の3つのインスタンス化方法を見てみます

// Langs<Lang[]>
const langs1 = new Langs(['TypeScript', 'Swift'] as Lang[])

// Langs<("TypeScript" | "Swift")[]>
const langs2 = new Langs(['TypeScript', 'Swift'])

// Langs<readonly ["TypeScript", "Swift"]>
const langs3 = new Langs(['TypeScript', 'Swift'] as const)
  1. as Lang[] にキャストしたとき、 T は要素数未確定の Lang[] になります
  2. キャストしていない配列リテラルのとき、 T は要素数未確定の ("TypeScript" | "Swift")[] になります
  3. as const にキャストした配列リテラルのとき、 T は要素数が確定した具体的な配列になります

このように、要素数を確定させたいときには as const でキャストする必要があり
さらに as const な値を受け取るには 不変 つまり readonly である必要があります

本題にもどり Find 型に readonly を対応させ ver1.0 を完成させまます

ver1.0
-type Find<Haystack extends any[], Needle extends any> =
+type Find<Haystack extends readonly any[], Needle extends any> =
  Needle extends Haystack[number] 
    ? 0 extends Haystack['length']
      ? Needle|undefined
      : Needle
    : undefined

以上で Find 型 の完成です

Discussion