メインコンテンツまでスキップ

タプル (tuple)

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

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

配列が抱える問題#

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

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

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

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

タプルの型#

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

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

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

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

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

タプルへのアクセス#

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

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

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

typescript
const list: [number, string, boolean] = tuple();
list[5];
// Tuple type '[number, string, boolean]' of length '3' has no element at index '5'.
typescript
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()の戻り値は分割代入を使うと次のように受けることができます。

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

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

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

タプルを使う場面#

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

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

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

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

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

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

これをPromise.all()を使うことで次のように書くことができます。このときかかる時間は関数の中でもっとも時間がかかる関数、つまり5秒です。

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

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

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

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