🫧

【TypeScript】TupleとArrayとそれ以外のケースで、よしなに型関数を適用する型

2023/03/27に公開

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>の数だけ書くことになる。isTupleutils/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