Conditional Types にて関数の第一引数により第二引数以降の候補を切り替える

# Conditional Types にて関数の第一引数により第二引数以降の候補を切り替える

以下のようなオブジェクトがあった場合に、
第一引数には `a`, `b`, `c` を、
第二引数には第一引数により取得されるオブジェクトの配下のキー (`a1`, `a2`, `b1`, ・・・) を、
第三引数には第二引数により取得されるオブジェクトの配下のキーを指定したいとします。

```js:multi-dimensional-object
const multiDimensional = {
  a: {
    a1: {
      AAA: 1,
      BBB: 2,
      CCC: 3,
    },
    a2: {
      DDD: 4,
      EEE: 5,
    },
  },
  b: {
    b1: 6,
    b2: 7,
  },
  c: 8,
}
```

## 実装

実装は以下のようになります。
JSDoc にて TypeScript を利用してますが JavaScript になります。

```js:multi-dimensional-access
const multiDimensional = {
  a: {
    a1: {
      AAA: 1,
      BBB: 2,
      CCC: 3,
    },
    a2: {
      DDD: 4,
      EEE: 5,
    },
  },
  b: {
    b1: 6,
    b2: 7,
  },
  c: 8,
}

/**
 * @typedef {typeof multiDimensional} MultiDimensional
 */
/**
 * 多次元オブジェクトで引数を切替
 * @template {keyof MultiDimensional} T1
 * @template {MultiDimensional[T1] extends object ? keyof MultiDimensional[T1] : undefined} T2
 * @template {T2 extends keyof MultiDimensional[T1] ? MultiDimensional[T1][T2] extends object ? keyof MultiDimensional[T1][T2] : undefined : undefined} T3
 * @param {T1} level1
 * @param {T2} [level2]
 * @param {T3} [level3]
 */
const multiDimensionalAccess = (level1, level2, level3) => {
  console.log(multiDimensional[level1])
  if (level2) {
    console.log(multiDimensional[level1][level2])
    if (level3) {
      console.log(multiDimensional[level1][level2][level3])
    }
  }
}
```

`a` 配下の場合 `level3` まで候補が存在します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/0e37fc4e-6023-a72d-a04d-3342b1ff0ca0.png)

`b` 配下の場合候補があるのは `level2` までです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/6f5f336c-9292-210b-435b-43e1e790e817.png)

`c` 配下は `level2` 以降は `undefined` となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/38a486b5-c377-13f9-3b9f-02946e9a0efb.png)

`a` 配下でも `level2` に `undefined` を指定した場合には `level3` の候補はなしです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116435/1ea06f37-012f-efe3-c514-c40dc0a4cacc.png)


## JSDoc の内容

### 型定義

`multiDimensional` を対象としてキーを候補としたいので `typeof` を利用して型定義を実施しています。

```ts:MultiDimensional
type MultiDimensional = typeof multiDimensional
```

### ジェネリクスの指定

引数の候補を切り替えるために各引数にはジェネリクスの指定をしています。
TypeScript でのつじつまを合わせるため記述が増えています。

- `level1` に指定する `T1` には `MultiDimensional` のキーを指定
- `level2` は `level1` により切り替わるため、 `MultiDimensional[T1]` がオブジェクトの場合にそのキーを指定
- `level3` は `level2` と同様

```ts:MultiDimensionalAccess
type MultiDimensionalAccess<
  // `level1` に指定する `T1` には `MultiDimensional` のキーを指定
  T1 = keyof MultiDimensional,
  T2 = T1 extends keyof MultiDimensional
    // `level2` は `level1` により切り替わるため、 `MultiDimensional[T1]` がオブジェクトの場合にそのキーを指定
    ? MultiDimensional[T1] extends object
      ? keyof MultiDimensional[T1]
      : undefined
    : undefined,
  T3 = T1 extends keyof MultiDimensional
    ? T2 extends keyof MultiDimensional[T1]
      // `level3` は `level2` と同様
      ? MultiDimensional[T1][T2] extends object
        ? keyof MultiDimensional[T1][T2]
        : undefined
      : undefined
    : undefined
> = (level1: T1, level2?: T2, level3?: T3) => void
```