TypeScriptでFind型を作ってみた
作ったもの
以下のテストが通る 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を作ります
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 を作ります
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)
-
as Lang[]
にキャストしたとき、T
は要素数未確定のLang[]
になります - キャストしていない配列リテラルのとき、
T
は要素数未確定の("TypeScript" | "Swift")[]
になります -
as const
にキャストした配列リテラルのとき、T
は要素数が確定した具体的な配列になります
このように、要素数を確定させたいときには as const
でキャストする必要があり
さらに as const
な値を受け取るには 不変 つまり readonly
である必要があります
本題にもどり Find
型に readonly
を対応させ 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