タプル (Tuple)

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

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

配列が抱える問題

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

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

ですが、このlist[n]からメソッドを呼ぶことができません。それはlistの各要素はunknownであるからです。

ではany[]を戻り値の型として使うべきかというと、それも問題です。せっかくTypeScriptを使って型による恩恵を享受しているのに、ここだけ型がないものとしてコーディングをするのも味気がありません。そこで使えるのがタプルです。

タプルの型

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

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

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

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

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

タプルへのアクセス

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

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

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

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

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

分割代入を使ってタプルにアクセスする

上記関数tuple()の戻り値は分割代入を使うと次のように受けることができます。

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()を使うことで次のように書くことができます。このときかかる時間は関数の中でもっとも時間がかかる関数、つまり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()
]);

Promise.all()は先に終了した関数から順番に戻り値のタプルとして格納されることはなく、元々の順番を保持します。take3seconds()の方が早く終わるから、先にタプルに格納されるということはなく、引数に渡した順番のとおりにタプルupleの要素の型は決まります。