タプル (Tuple)

TypeScriptの関数は1値のみ返却可能です。戻り値に複数の値を返したい時に、配列を返しつつ中に様々な値を入れることがあります。なお以下の関数の戻り値は定数になっていますが、実際は演算した結果だと解釈してください。

function tuple() {
//...
return [1, 'ok', true];
}

極端な例ですが、上記のような時がないとは言えません。

配列が抱える問題

上記例では戻り値の型として何が妥当でしょうか。配列のページから読み進めていただいた方はなんでも入れられる型、ということでany[]またはunknown[]が型の候補として思い浮かぶ人もいるかと思います。

const list: unknown[] = tuple();
list[0].

ですが、このlist[0]の後に.をつけてもTypeScriptは入力補完をしませんし、メソッドを呼ぶことができません。それはlistの要素はunknownであり、つまりTypeScriptはそれがどの型であるかを関心しないからです。

せっかくTypeScriptを使って型による恩恵を享受しているのに、ここだけ型がないものとしてコーディングをするのも味気がありません。そこで使えるのがタプルです。

タプルの型

タプルの型は簡単です。[]を書いて中に型を書くだけです。つまり、上記関数tuple()は以下のような戻り値を持っていると言えます。

const list: [number, string, boolean] = tuple();

同様に関数の戻り値も書くことができます。

function tuple(): [number, string, boolean] {
//...
return [1, 'ok', true];
}

配列の型はarray, genericというふたつの書き方がありましたがタプルはこの書き方しか存在しません。

タプルへのアクセス

タプルを受けた変数はそのまま中の型が持っているプロパティ、メソッドを使用できます。

const list: [number, string, boolean] = tuple();
list[0].toExponential();
list[1].length;
list[2].valueOf();

toExponential(), length, valueOf()はそれぞれnumber型、string型、boolean型のプロパティ、メソッドです。

タプルを受けた変数は、タプルで定義した範囲外の要素に対してアクセスができません。

const list: [number, string, boolean] = tuple();
list[5];
// -> Tuple type '[number, string, boolean]' of length '3' has no element at index '5'.

そのためlist.push()のような配列の要素を増やす操作をしてもその要素を使うことはできません。

Destructuring assignmentを使ってタプルにアクセスする

上記関数tuple()の戻り値はDestructuring assignmentを使うと以下のように受けることができます。

const [num, str, bool]: [number, string, boolean] = tuple();

また、特定の戻り値だけが必要である場合は変数名を書かず,だけを書きます。

const [,, bool]: [number, string, boolean] = tuple();

タプルを使う場面

TypeScriptで非同期プログラミングをする時に、時間のかかる処理を直列ではなく並列で行いたい時があります。その時TypeScriptではPromise.all()というものを使用します。この時タプルが役に立ちます。 Promiseについての詳しい説明は本書に専門の頁がありますので譲ります。ここではPromise<T>という型の変数はawaitをその前につけるとTが取り出せることだけ覚えておいてください。また、このTをジェネリクスと言いますが、こちらも専門の頁があるので譲ります。

const promise: Promise<number> = yyAsync();
const num: number = await promise;

例えば以下のような処理に時間が3秒、5秒かかる関数takes3Seconds()takes5Seconds()があるとします。

async function takes3Seconds(): Promise<string> {
// ...
return 'finished!';
}
async function takes5Seconds(): Promise<number> {
// ...
return -1;
}

この関数をそのまま実行すると3 + 5 = 8秒かかってしまいます。

const str: string = await takes3Seconds();
const num: number = await takes5Seconds();

これをPromise.all()を使うことで以下のように書くことができます。この時かかる時間は関数の中で最も時間がかかる関数、つまりMath.max(3, 5) = 5秒です。

const tuple: [string, number] = await Promise.all([
takes3Seconds(),
takes5Seconds()
]);

この時Promise.all()の戻り値を受けた変数tuple[string, number]型です。実行する関数のPromise<T>のジェネリクスの部分とタプルの型の順番は一致します。つまり以下のように入れ替えたら、入れ変えた結果のタプルである[number, string]型が得られます。

const tuple: [number, string] = await Promise.all([
takes5Seconds(),
takes3Seconds()
]);

take3seconds()の方が早く終わるから、先にtupleに入るということはありません。引数に渡した順番の通りにtupleの要素の型は決まります。