TypeScriptで配列を引数に取り配列を返す関数を型定義する

投稿日時:3/24/2021, 2:48 PM

はじめに

TypeScriptで引数と返り値が配列となる関数に型付けする方法についてまとめる。この記事の内容は先日Qiitaに書いた以下の記事がベースになっているが、あちらは最終的な結果から逆算して必要最低限で書いたところがあるので、こちらではもう少し初歩の内容から始めて、色々書いていきたいと思う。

TypeScriptで配列を引数に取り配列を返す関数を型定義する

配列の要素がプリミティブ型の場合

まず初歩の内容として、string[] 型の配列を引数に取り、そのまま string[] 型の配列を返すような関数を考える。この関数は以下のように型定義できる。

本記事では引数の配列を beforeList, 返り値の配列を afterList と名付け、関数の名前を convert としている。

function convert(beforeList: string[]): string[];

これは配列の記法を知っていれば書けるのでなんてことはないと思う。では次に配列の要素が複数の型を取りうる場合について考えたい。

配列の要素を複数のプリミティブ型に対応させる場合

複数の型に対応することを考えると、自分がどういう型を定義したいのかもう少し詳細を詰める必要がある。例えば、「引数の配列の要素が string 型でも number 型でも良い」と考えたときに、以下の2つの場合が考えられる。

  • 配列の要素に string 型と number 型が入り乱れている場合
  • 配列の要素は string 型か number 型どちらか一方であるが、1つの型定義でそのどちらの場合にも対応したい場合

配列の要素にstring型とnumber型が入り乱れている場合

これも初歩的な知識で書くことができる。

function convert(beforeList: Array<string | number>): Array<string | number>;

const afterlist = convert([1, 'a', 2, 'b']); // afterList は Array<string | number> 型になる

Union型を使うだけなので、レベル的には最初の例と変わらない。

配列の要素はstring型かnumber型どちらか一方であるが、1つの型定義でそのどちらの場合にも対応したい場合

この場合はジェネリクスを使って関数を型定義する必要がある。一つ前の例と何が違うかというと、一つ前の例は今の場合と同様、配列の要素を string 型、もしくは number 型に絞れてはいたが、絞って終わりで、では実際それが string 型なのか、number 型なのか追跡はしなかったし、そうする術もなかった。その追跡をするためには、まず引数の型を型変数 T で受ける必要がある。

function convert<T>(beforeList: T): T; // 返り値は引数と同じ T 型とする

上の例では、先程まで string | number で定義していた関数の引数を T という型変数で受けている。T が型変数であることを示すために、関数引数 () の前に <T> という宣言を追加している。引数の型を T という変数で受けたことで、返り値の型にもその情報を使うことができている。

これでこの convert という関数はどんな型でも受けられるようになった。しかし、やりたかったのは string 型の配列、もしくは number 型の配列を受け取り、返り値としてそれぞれ引数と同じ型の配列を返すことだった。「どんな型の引数でも受けられる」というのでは型として緩い。そこで型引数 T に制約を与えるため、次のような書き方をする。

function convert<T extends string[] | number[]>(beforeList: T): T;

これで、先程宣言した型引数 Textends string[] | number[] という制約を満たさなければならなくなった。この extends が意味するところは、T 型が string[] | number[] の部分型かどうか、言い換えると、T 型を string[] | number[] 型に代入できるかどうか、を引数の制約にするということである。いま、引数に想定しているのは string[] 型、もしくは number[] 型なので、これらは string[] | number[] の部分型という条件を満たしている。逆に、例えば Tanystring が入ると、それらは string[] | number[] の部分型ではないので、制約違反ということになり、型エラーになる。よって、これは狙い通りの制約である。

React のような完成されたライブラリを使っている限りは、このレベルの型定義が必要になる場面もまず無いので、自分はUnion型、Intersection型を学んだあとなかなかその先に進めなかった。extendsinterface を継承するときに使うことがあるくらいで、条件として使う場面はやはりほぼないので入門者は最初の壁に感じると思う。

配列の要素にstring型とnumber型が入り乱れている場合の発展

ここからは前に考えた string 型と number 型が入り乱れている場合の発展として、その個々の要素の型情報を維持したまま返り値に型をつける方法を考える。これを実現するためには、これまでの配列型ではなく、Tuple型を使う必要がある。そのため、これから定義する関数はプログラム実行時に動的に生成した配列を引数に渡すことはできない。ただ、コーディング時に要素数が確定している配列に対してはそれをTuple型で定義することで引数に渡すことができる。

TypeScriptのタプル型は配列型の中でも、要素数が固定されており、それぞれの要素の型が定義されたものと考えることができる

このような型はVariadic Tuple TypesというTypeScriptの機能を使って以下のように書くことができる。

function convert<T extends ReadonlyArray<string | number>>(beforeList: [...T]): [
    ...{ [K in keyof T]: T[K] extends string ? string : number}
];

const afterList = convert([1, 'a', 1]) // afterList は [number, string, number] 型になる

上では convert の引数の中で [1, 'a', 1] と書いているので、これがタプル型と判断されている。ポイントは Variadic Tuple Types によって任意の要素数のタプルの中身を ...T で受けられていることである。受けた後は T の各要素にインデックスアクセスすることができ、例のように extendsstirng の部分型かどうかを見て、返り値の型を出し分けることができる。

なお、引数のタプルを関数の外で定義する場合は以下のように書く。

const beforeList: [number, string, number] = [1, 'a', 1];
const afterList = convert(beforeList);

配列の要素がジェネリック型の場合

上の例をもう少し応用して、あるジェネリック型のTupleを引数に取り、型変数を引き継いだまま、別のジェネリック型のTupleを返す関数に型をつけることができる。これについては最初にも貼った以下の記事で書いているので、結果だけ書くことにする。

// サンプルとして Before<T> 型と After<T> 型を定義
type Before<T> = {
    type: 'before',
    value: T,
}

type After<T> = {
    type: 'after',
    value: T,
}

// convert の型定義 (関数本体の実装は省略)
function convert<T extends ReadonlyArray<Before<unknown>>>(beforeList: [...T]): [
    ...{ [K in keyof T]: T[K] extends Before<infer U> ? After<U> : never }
]

// テスト
const before1: Before<number> = {
    type: 'before',
    value: 1,
}

const before2: Before<string> = {
    type: 'before',
    value: 'string',
}

const afterList = convert([before1, before2]); // [After<number>, After<string>] 型になっている

Tuple型で定義できるもの、という制約はあるものの、JSライブラリの中にはオブジェクトの配列から別オブジェクトの配列を作る関数を提供しているものがあったりするので、そういう関数に型定義する場面ではこの方法は役に立つことがあると思う。


share on...