🫧
【TypeScript】TupleとArrayとそれ以外のケースで、よしなに型関数を適用する型
OCP(開放閉鎖の原則)に則った拡張性の高さを意識したコードを書いている時に出てきがちな、
- ある型がtupleの場合、 たとえば
[number, number]と型関数F<T>から[F<number>, F<number>]を作りたい - ある型が配列の場合、 たとえば
number[]と型関数F<T>からF<number>[]を作りたい - それ以外の場合、たとえば
{ id: number }と型関数F<T>からF<{ id: number }>を作りたい
を実現する型の書き方は次のようになる
type IsTuple<T> = T extends readonly any[]
? number extends T['length']
? false
: true
: false
type ApplySomeFunc<I> =
IsTuple<I> extends true
? {[K in keyof I]: F<I[K]>}
: I extends Array<infer U>
? Array<F<U>>
: F<I>
TypeScriptでは型関数の引数に型関数を渡すことはできない(渡したい…)ので、ApplySomeFuncに相当する型関数はF<T>の数だけ書くことになる。isTupleはutils/type.ts的なところに入れておけばいい。
Iがtupleの時(たとえば、[number, number]とする)に{[K in keyof I]: SomeFunc<I[K]>}と書くことで、[F<number>, F<number>]が得られる挙動は、Mapped Tuple Typeと呼ばれる、Mapped Type とは少し異なるイレギュラーな挙動である。
type SomeTuple = [number, number]
type A = Tuple['map'] // これは型エラーにはならない
このように作成したSomeTuple型は、Array.prototypeが持っているメソッドを型情報として持つ。そのため、Mapped Typeのルールに従うと{[K in keyof I]: SomeFunc<I[K]>}はatなどのArray由来のプロパティを持つはずだが、実際にはそうはならない。ルールの一貫性より使いやすさが優先された結果、この挙動が追加された。(そのときのリリースノート)
参考にさせていただいた記事:
Discussion