TypeScript 3.1 の Mapped Tuple Typeについて


気付いたらTS 3.1.1がしれっとリリースされていたので、今回導入されたMapped Tuple Typeについて書いてみようと思います。

対応するPRは[こちら](https://github.com/Microsoft/TypeScript/pull/26063)。

今回のMapped Tuple Typeは、TypeScript 2.1で導入されたMapped Typeの強化版です。
ちなみに「Mapped Type is 何」という人は、以前に[解説記事](https://qiita.com/Quramy/items/e27a7756170d06bef22a)を書いたのでこれを読むと良いかと。

さて、Mapped Tuple Type、別に新しい記法とかが追加されたわけではありません。

次のコードは上記のPRに記載されているサンプルです。

```ts
type Box<T> = { value: T };
type Boxified<T> = { [P in keyof T]: Box<T[P]> };

type T1 = Boxified<string[]>;  // Box<string>[]
type T2 = Boxified<ReadonlyArray<string>>;  // ReadonlyArray<Box<string>>
type T3 = Boxified<[number, string?]>;  // [Box<number>, Box<string>?]
type T4 = Boxified<[number, ...string[]]>;  // [Box<number>, ...Box<string>[]]
type T5 = Boxified<string[] | undefined>;  // Box<string>[] | undefined
type T6 = Boxified<(string | undefined)[]>;  // Box<string | undefined>[]
```

`type Boxified<T> = { [P in keyof T]: Box<T[P]> };` の行がMapped Typeの記法です。
この記法は3.0以前となんら変わりはありません。

問題はこの型パラメータ `T` に配列が代入された場合です。
従来は、 `keyof string[] = "pop" | "push" | "concat" | "join" | "reverse" | "shift" | "slice" // 以下略` のようにArrayに定義されているメソッド名に対してMapされてしまいました。
これが3.1以降では、あたかも `keyof string[] = number` のように振る舞います。

といっても、 `keyof` の意味に変更がなされたわけではなく、飽くまでMapped Typeのマッピング挙動が変更された、ということのようです。

マッピング解釈の新しいルールは次のようになっています。

* `T` が基本型の場合、マッピングは行われない
* `T` がunion typeの場合、それぞれのMapped Typeのunionに分配される
* `T` が `S[]` の場合、`R[]` にマッピングする。 `R` は `T[P]` を `S` に置き換えた `X` のインスタンス
* `T` が `ReadOnlyArray<S>` の場合、 `ReadOnlyArray<R>` にマッピングする。 `R` は `T[P]` を `S` に置き換えた `X` のインスタンス
* `T` が `[S0, S1, ..., Sn]` である場合、 `[R0, R1, ..., Rn]` にマッピングする。`Rx` は `T[P]` を `Sx` に置き換えた `X` のインスタンス

要するに「Array Likeな型のMapped Typeは、その配列の要素に対してマッピングが行われる」と解釈しておけば大体あっているはず。

このMapped Tuple Typeがどこで役に立つかというと、 やはりBoxing/Unboxingと配列が絡むような関数の型定義でしょう。

たとえばrxjsには `combineLatest` という関数が定義されています。
こいつは複数個のObservableを引数として受け取ると、それらのストリームの最新の値のタプルのストリームを返してくれる関数です。

この関数をバカ正直に型定義すると、次のようになります。

```ts
declare namespace rx {
  interface Observable<T> {
    subscribe(cb: (v: T) => void): void;
  }
  function combineLatest(...streams: Observable<any>[]): Observable<any[]>;
}
```

でもこれは `any` ばかりになってしまい、全然型の恩恵を受けられないので、こういった関数の型定義は次のように書いたりしていました。

```ts
declare namespace rx {
  interface Observable<T> {
    subscribe(cb: (v: T) => void): void;
  }
  function combineLatest<T1, T2>(s1: Observable<T1>, s2: Observable<T2>): Observable<[T1, T2]>;
  function combineLatest<T1, T2, T3>(s1: Observable<T1>, s2: Observable<T2>, s3: Observable<T3>): Observable<[T1, T2, T3]>;
  // 大体T6とかT7くらいまで続いてたりする
  function combineLatest(...streams: Observable<any>[]): Observable<any[]>;
}
```

この関数をMapped Tuple Typeを使って書き直すと、次のようになるわけですね。

```ts
declare namespace rx {
  interface Observable<T> {
    subscribe(cb: (v: T) => void): void;
  }

  type StreamValue<S> = S extends Observable<infer T> ? T : never;

  type Unwrap<S> = { [P in keyof S]: StreamValue<S[P]> };

  function combineLatest<U extends Observable<any>[]>(...streams: U): Observable<Unwrap<U>>;
}
```

今回は `Observable<T>` の中身を取り出したいので、「Observableの中身を取り出すConditional Type」をMapped Typeに食わせています。

下記のようなテストコードを書いて、`s1` や `s2` の型をVSCなどのツールチップで見てみてると、ちゃんと任意個数の引数について、中身の型を取り出せていることが確認できます。

```ts
function test(s1$: rx.Observable<string>, s2$: rx.Observable<number>, s3$: rx.Observable<string>) {
  rx.combineLatest(s1$, s2$).subscribe(([s1, s2]) => console.log(s1, s2));
  rx.combineLatest(s1$, s2$, s3$).subscribe(([s1, s2, s3]) => console.log(s1, s2, s3));
}
```

そういえば今回の変更は「Mapped Typeの解釈が変わった」という意味ではBreaking Changeじゃないの？と思わないでもないですが、[3.1 RC アナウンスのblog](https://blogs.msdn.microsoft.com/typescript/2018/09/13/announcing-typescript-3-1-rc/) には次のように書いてありました。

> While technically consistent in behavior, the majority of our team felt that this use-case should just work. Rather than introduce a new concept for mapping over a tuple, mapped object types now just “do the right thing” when iterating over tuples and arrays. 

わざわざ新しい記法を導入するよりも「大多数の人が望む挙動」に修正することを選んだ、とのことらしいです。