TypeScript：関数の引数に同じ長さの配列を渡したい

# やりたいこと

```ts
requireTwoSameLengthArrays([1, 2], [3, 4]); // ok
requireTwoSameLengthArrays([1, 2], [3]); // 型エラー
requireTwoSameLengthArrays([1, 2], [3, 4, 5]); // 型エラー
```

になるような関数を作りたい


# 結論


```ts
function requireTwoSameLengthArrays<
    T extends (readonly [] | readonly any[]) & (
        number extends T["length"] ? readonly [] : unknown
    )>(t: T, u: { [K in keyof T]: any }){
//
}
```


# ステップ１：最低限の解答

```ts
function requireTwoSameLengthArrays<
    T extends [] | any[]
>(t: T, u: { [K in keyof T]: any }) { 
  //
}
```


## 解説

```ts
T extends [] | any[]
```
の箇所について。`any[]`は`[]`も内包しているので

```ts
T extends any[]
```

だけでいいのではないか？と思うかも知れませんが，これは**コンパイラーに固定長のタプル型として認識させる**ために両方必要です。


<details>
<summary>ジェネリクスの配列・タプルの例</summary>
<div>

```ts

function func1<T extends any[]>(arg: T) {
  return arg;
}
const a = func1([1,2,3]) //a: number[]
const b = func1([1,2,3] as const) //エラー: The type 'readonly [1, 2, 3]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.

function func2<T extends  readonly any[]>(arg: T) {
  return arg;
}
const c = func2([1,2,3]) //c: number[]
const d = func2([1,2,3] as const) //d: readonly [1, 2, 3]


function func3<T extends any[] | []>(arg: T) {
  return arg;
}
const e = func3([1,2,3]) //e: [number, number, number]


```
</div>
</details>


そのため，１つ目の引数tに`[1,2]`が入ると，Tは`[number,number]`であると推論されます。


```ts

u: { [K in keyof T]: any }
```

はmapped typeと呼ばれますが，Tに配列を入れると少し変わった挙動をして(**Mapped Tuple Type**と呼ばれます)，uはTの配列長と同じ(今回の場合は配列長２)の`any[]`型となります。



<details>
<summary>Mapped Tuple Typeについて詳しく</summary>
<div>

公式の記事はこちら
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html
以下の例のように，１つ１つの要素に対してマッピングされ，新しいタプルや配列を生成します。

```ts
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };

type Coordinate = [number, number];

type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]
```

そこで，今回使われているように

```ts
const a: {[K in keyof [number,number]]: string}
```

としたときのaの型を見てみると，以下のように配列長が２の`string[]`型になっていることが確認できます。

```ts
const a: {
    [x: number]: string;
    0: string;
    1: string;
    length: string;
    toString: string;
    toLocaleString: string;
    pop: string;
    push: string;
    concat: string;
    join: string;
    reverse: string;
    shift: string;
    slice: string;
    sort: string;
    ... 20 more ...;
    flat: string;
}

```

</div>
</details>


# ステップ２：さらに拡張性のある解答

今のままだと以下のときにエラーが発生します。

```ts
requireTwoSameLengthArrays([1, 2] as const, [3, 4]);  
//エラー：The type 'readonly [1, 2]' is 'readonly' and cannot be assigned to the mutable type 'any[]'.
```

以下のように改善するとエラーが発生しなくなります。

```ts
function requireTwoSameLengthArrays<
    T extends readonly [] | readonly any[]
>(t: T, u: { [K in keyof T]: any }) { 
  //
}
```

# ステップ３：さらにさらに拡張性のある解答

今のままでは以下のような問題があります。

```ts
const arr = [1, 2];
requireTwoSameLengthArrays(arr, [1, 2, 3]); //型エラーが起きてほしいのに起きない！！！！！！！
```

これは

```ts
const arr = [1, 2];
```
の時点で`arr`が`number[]`と推測されてしまっているからです。

以下のように改善すると期待通りの挙動になります。

```ts

function requireTwoSameLengthArrays<
    T extends (readonly [] | readonly any[]) & (
        number extends T["length"] ? readonly [] : unknown
    )>(t: T, u: { [K in keyof T]: any }){
//
}
```

これを使うと以下のような挙動をします。

```ts

const arr = [1, 2];
requireTwoSameLengthArrays(arr, [1, 2, 3]);
// エラー：Argument of type 'number[]' is not assignable to parameter of type '(readonly any[] | readonly []) & readonly []'.

const arr = [1, 2] as const;
requireTwoSameLengthArrays(arr, [1, 2, 3]); 
// エラー：Argument of type '[number, number, number]' is not assignable to parameter of type 'readonly [any, any]'.

const arr = [1, 2, 5] as const;
requireTwoSameLengthArrays(arr, [1, 2, 3]); //ok

```

## 解説

Tが`T["length"]`を持っているかどうかをチェックして、もし`T["length"]`が`number`であれば空のタプルを要求することでエラーを発生させています。



# 参考
https://stackoverflow.com/questions/62206320/typescript-require-that-two-arrays-be-the-same-length