【TypeScript】配列から取得した値の型について

# 配列やオブジェクトの型推論
TypeScriptでは配列やオブジェクトの一部にアクセスすると、その値が存在すると仮定した型に推測されます。例えば以下のような関数を考えます。

```ts
// sample1の返り値の方はstringと推論される
const sample1 = (arg: string[]) => {
  return arg[0];
};

// sample2の返り値の方はnumberと推論される
const sample2 = (arg: Record<string, number>) => {
  return arg.value;
};
```
それぞれ定義された型の一部がそのまま返り値として推測されます。しかし、`sample1`の引数に空配列が渡された場合、`sample2`に渡された引数のオブジェクトに`value`をキーにもつ値が存在しなかった場合はこれらの値は推論された型と矛盾して`undefined`となります。
つまり、配列やオブジェクトにアクセスしたときはデフォルトで推論される型と`undefined`とのユニオン型として推測されて欲しいです。今回のケースだと`sample1`の返り値は`string | undefined`に、`sample2`の返り値は`number | undefined`となるべきです。
このような`undefined`とユニオンを取らせるような型推論はTypeScriptの設定ファイルで[noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess)を`true`にすることで切り替えることができます。予期せぬバグなどを防げるので是非有効にして開発していきましょう。

# 型の絞り込み
`noUnCheckedIndexedAccess`を有効にしたことで、配列から取得した値が`undefined`とのユニオンをとるようになるので扱いが大変になります。
以下の配列を考えましょう。
```ts
const sample: number[] = [1, 2, 3, 4];
```
設定を有効にしている場合`sample[0]`の型は`number | undefined`に推論されます。値を`number`として扱いたいとき、毎回`undefined`ではないことを示すコードを書く必要があります。
これを防ぐためには`sample`の型を`[number, ...number]`のようにすれば、`sample`に確実に0番目の値が存在することを型で保証されます。この型の場合、`sample[0]`は`number`と推論されます。
それを元に以下のコードを見ていきます。
```ts
const numToStr = (num: number): string => {
  return num.toString();
};

const convertFirstArgToStr = (arg: number[]): string => {
  if (arg.length > 0) {
    return numToStr(arg[0]);
  }

  return '';
};
```
`convertFirstArgToStr`関数は`numToStr`を呼び出す箇所で型エラーが現れます。これは`arg[0]`の型が`number | undefined`となっているため、引数に`number`型をとる`numToStr`に代入できないからです。
エラーが発生した箇所は`arg`に少なくとも1以上の長さが存在することを確認した分岐内なので0番目の値は`number`に推論されてほしいです。しかし、分岐によって`arg`の型が変更(再推論)されないためそのようにはなりません。

これを解決するために、配列の長さを確認した分岐内では保証した長さまで値が存在するような型として扱う関数`satisfiesLengthRequirement`を作成します。
```ts
type LengthArray<
  T,
  U extends number,
  V extends unknown[] = [],
  Result extends T[] = [],
> = V['length'] extends U
  ? [...Result, ...T[]]
  : LengthArray<T, U, [...V, unknown], [...Result, T]>;

const satisfiesLengthRequirement = <T, U extends number>(
  array: T[],
  requiredLength: U,
): array is LengthArray<T, U> => {
  return array.length >= requiredLength;
};

const numToStr = (num: number): string => {
  return num.toString();
};

const sample = (arg: number[]): string => {
  if (satisfiesLengthRequirement(arg, 1)) {
    return numToStr(arg[0]);
  }

  return '';
};
```
これによって分岐内では`args`の型が`[number, ...number[]]`と推論されるようになり、型エラーを除くことができました。
今回行った変更についてまずは、型`LengthArray`を解説します。
`LenghtArray`は`T`が値の長さ`U`まで値があることを保証する配列を返す型です。`T`が`number`、`U`が`3`の時は`[number, number, number, ...number[]]`が返ってきます。
```ts
type LengthArray<
  T,
  U extends number,
  V extends unknown[] = [],
  Result extends T[] = [],
> = V['length'] extends U
  ? [...Result, ...T[]]
  : LengthArray<T, U, [...V, unknown], [...Result, T]>;

// [number, number, number, ...number[]]
type Sample = LengthArray<number, 3>;
```
呼び出す時に必要な型引数は配列の値`T`と保証する長さ`U`の2つになります。他の型引数`V`と`Result`は内部で再帰的に呼び出すときに利用します。
`LengthArray`は`V`の長さが`U`と同じ長さになるまで自身を再帰的に呼び出します。呼び出すたびに、`V`に`unknown`、`Result`に`T`を1つずつ追加します。最終的に`V`が成長して`U`と同じ長さになった時はこれまで作ってきた`Result`に`...T[]`を追加したものを返します。
`LengthArray<T, 3>`のように呼び出された場合はループの度に以下のような状態を持ちます。
| T | U | V | Result | 返り値 |
| --- | --- | --- | --- | --- |
| `T` | `3` | `[]` | `[]` | `LengthArray<T, 3, [unknown], [T]>` |
| `T` | `3` | `[unknown]` | `[T]` | `LengthArray<T, 3, [unknown, unknown], [T, T]>` |
| `T` | `3` | `[unknown, unknown]` | `[T, T]` | `LengthArray<T, 3, [unknown, unknown, unknown], [T, T, T]>` |
| `T` | `3` | `[unknown, unknown, unknown]` | `[T, T, T]` | `[T, T, T, ...T[]]` |

この型を扱う時は`U`が1000以上の場合にTypeScriptの定義する再帰数の上限値を超えたことによるエラーが発生するので気をつけてください。
次に`satisfiesLengthRequirement`です。
```ts
const satisfiesLengthRequirement = <T, U extends number>(
  array: T[],
  requiredLength: U,
): array is LengthArray<T, U> => {
  return array.length >= requiredLength;
};
```
これは`array`が`requiredLength`以上であることを確認する関数です。`T`は`array`の値、`U`は`requiredLength`の型になります。
そして、`array is LengthArray<T, U>`のように返り値を定義しています。`is`は[ユーザー定義型ガード](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)と呼ばれていて、この場合は関数が`true`と返したスコープでは`array`の型を`LengthArray<T, U>`とみなすような働きをします。
これによって分岐内では`arg[0]`の型が`number`に推論されます。
```ts
const sample = (arg: number[]): string => {
  if (satisfiesLengthRequirement(arg, 1)) {
    // このスコープではargの型が[number, ...number[]]
    return numToStr(arg[0]);
  }

  return '';
};
```
この関数を用いれば配列の長さが少なくとも`n`はあることを保証して、その長さまでの値を取得したときに`undefined`とのユニオンを作らせずに扱えます。

:::note warn
ユーザー定義型ガードは厳密に型安全な機能ではないことに注意してください。
`satisfiesLengthRequirement`の実装が以下のようになっていた場合、実際にそこに値が存在していることを保証せずに`array`の型を`LengthArray<T, U>`のように扱います。
```ts
const satisfiesLengthRequirement = <T, U extends number>(
  array: T[],
  requiredLength: U,
): array is LengthArray<T, U> => {
  return true;
};
```
利用する際はその関数のロジックが正しいことと、ロジックに対する型が厳密に正しいことを確認してから行うようにしてください。
:::

何らかの要因でそこに存在する配列のn番目までは値が存在すると約束されているとします。その場合は[アサーション関数](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions)を利用して型を絞り込みます。
```ts
type LengthArray<
  T,
  U extends number,
  V extends unknown[] = [],
  Result extends T[] = [],
> = V['length'] extends U
  ? [...Result, ...T[]]
  : LengthArray<T, U, [...V, unknown], [...Result, T]>;

// アロー関数だと関数に型を与えないとエラーになる
function satisfiesLengthRequirement<T, U extends number>(
  array: T[],
  requiredLength: U,
): asserts array is LengthArray<T, U> {
  if (array.length < requiredLength) {
    throw new Error(`arrayの長さは${requiredLength}以下である必要があります`);
  }
}

const sample = (arg: number[]): string => {
  satisfiesLengthRequirement(arg, 1);
  return numToStr(arg[0]);
};

```
先程とほとんど同じ例ですが、今回は`arg`の0番目が絶対にあるという条件下なので、分岐などはなく呼び出し後に`arg`の型が`[number, ...number[]]`と推論されます。こちらも先ほどと同様で厳密な型の安全性を保てないのでロジックが正しいことを確認して利用してください。