asyncとawaitに触れてみた

## はじめに
業務で Node.js を使って対話型CLIツールを実装してみました。
その時に async、await と向き合い、自分なりに理解したことを残しておきます。

## やりたいこと
対話型CLIにて得た回答からCSVを作成する

## 環境構築
WindowsのWSLにて動作確認しています

### 1. [volta](https://volta.sh/) のインストール
```
curl https://get.volta.sh | bash
volta --version
```
Node.js のバージョン管理ツール
今回作成するアプリは、OSが混じる可能性があるため、voltaを選択
他の選択肢でいうと、[nodeenv](https://www.npmjs.com/package/nodeenv)、[nvm](https://www.npmjs.com/package/nodeenv) がある

### 2. node, typescript のインストール
```
volta install node
volta install typescript
```

### 3. ts-node のインストール
```
npm install ts-node
```

Node.jsはJavaScriptを実行できるが、TypeScriptを直接実行することはできない
ts-nodeを利用するとTypeScriptのファイルを指定して実行することができる

package.jsonに以下を追加することで、　`npm start` コマンドにて、トランスパイルと実行を行うことができるようになる
```
"scripts": {
    "start": "ts-node ./src/main.ts"
}
```

### 4. [inquirer.js](https://www.npmjs.com/package/inquirer)(対話型CLIライブラリ)のインストール

```
npm install inquirer
```
:::note info
[ドキュメント](https://www.npmjs.com/package/inquirer#methods:~:text=Documentation-,Installation,-npm%20install%20%2D%2Dsave)には、`npm install --save inquirer` と記載されていますが、npm 5.0.0 以降からは install 時にデフォルトで save してくれるので --save や -S オプションを指定する必要はない。
:::

`require('inquirer') `がエラーになるので、[ドキュメント](https://www.npmjs.com/package/inquirer#methods:~:text=Inquirer%20v9%20and,upgrade%20your%20environment%3A)にあるとおり、以下を実行

:::note warn
記事公開時には、ドキュメントのリンク先の情報がupdateされており、当時の情報とは異なっています。
:::

```
npm install inquirer@^8.0.0
npm install --dev @types/inquirer@^8.0.0
```

## 実装
### First Step

まずはREADMEを見ながら動かしてみる
ユーザに入力/選択を促す質問を作成

#### code
```typescript
import inquirer, { Answers } from "inquirer";

const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
  {
    type: "input",
    name: "name",
    message: "What is your name or nickname?",
  },
  {
    type: "checkbox",
    name: "hobbies",
    message: "Select your hobbies:",
    choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
  },
];

inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
  console.log(answers);
  return answers;
});
```

#### 実行

途中経過
```bash
npm start

> start
> ts-node ./src/main.ts

? What is your name or nickname? mayumi
? Select your hobbies: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◯ Reading
 ◯ Traveling
 ◯ Cookiing
❯◉ Sports
 ◯ Music
 ◯ Others
```

結果
```bash
? What is your name or nickname? mayumi
? Select your hobbies: Sports
{ name: 'mayumi', hobbies: [ 'Sports' ] }
```

うん、なんかよさげ。

### Second Step
質問の回答によって、処理を分岐してみる
1. 趣味に`スポーツ`を選択した場合には、どのスポーツが好きかの質問(選択式)をしたい
1. 趣味に`その他`を選択した場合には、趣味を入力(記述式)してもらいたい

とりあえず1だけ実装してみる...

### code
```typescript
import inquirer, { Answers } from "inquirer";

const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
  {
    type: "input",
    name: "name",
    message: "What is your name or nickname?",
  },
  {
    type: "checkbox",
    name: "hobbies",
    message: "Select your hobbies:",
    choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
  },
];

const sportsQuestion = [
  {
    type: "checkbox",
    name: "sports",
    message: "Select your favorite sports:",
    choices: [
      "Baseball",
      "Soccer",
      "Basketball",
      "Volleyball",
      "Badminton",
      "Tennis",
      "Others",
    ],
  },
];

const othersQuestion = [
  {
    type: "input",
    name: "other_hobbies",
    message: "What your hobbies?",
  },
];

inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
  if (answers.hobbies == "Sports") {
    inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
      const allAnswers = { ...answers, ...sportsAnswers };
      console.log(allAnswers);
    });
  }
  console.log(answers);
  return answers;
});

```
やりがち：とりあえず console.log で様子見...

### 実行
![step2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6481/685de0bf-309f-fba3-9ec5-ee24a09e622c.gif)

なんか思ってるのとちょっと違う...🤔 console.logされるタイミングが変...

#### つぶやき
あ、そっか。2つ目のconsole.log してる場所が悪いのか...
promiseを返すってことは、inquirer.promptは非同期処理なので、そっか先にconsole.log走るよね。
頭ではわかってるつもりだったけど、実感として少し理解。

というわけで呼び出し部分を修正...

```typescript
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
  if (answers.hobbies == "Sports") {
    inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
      answers = { ...answers, ...sportsAnswers };
      console.log(answers);
    });
  } else {
    console.log(answers);
  }
});

```
### 実行

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6481/bc837182-ed4b-cf6b-44f1-7059f21c230a.gif)

うん、なんかよさげ。

いやでもさ...

#### つぶやき(再び)
🤔 これ、分岐が増えるたびに then が増えて、入れ子がとんでもないことになるよね...
🧚 ねね、前に `await つけると Promise 外せる` って typescript めっちゃできる人が言ってたよね。これってそういうことじゃ...？

よし、ちょっとやってみよう。

## async/await 使ってみる

まずは、`inquirer.prompt` を関数化してみる
作成したいのは、**引数で指定したquestionに対するanswerを返す関数**

```typescript
const ask = async (
  questions: inquirer.QuestionCollection<inquirer.Answers>
): Promise<inquirer.Answers> => {
  const answers = await inquirer.prompt(questions);
  return answers;
};
```

<details><summary>ask関数作成の過程を見たい方はこちら</summary>

![ask_function.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6481/6fa14eaa-f6b6-48bf-b73f-bb70a40f4139.gif)

async/awaitはセットで使いますが、とりあえず同期処理にしたいところにawaitつけたらvscodeが教えてくれます。

</details>


元のソースを　`inquirer.prompt` の直接呼び出しから、ask 関数を呼ぶように変更

```typescript
inquirer.prompt(commonQuestions).then((answers: inquirer.Answers) => {
  if (answers.hobbies == "Sports") {
    inquirer.prompt(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
      const allAnswers = { ...answers, ...sportsAnswers };
      console.log(allAnswers);
    });
  } else {
    console.log(answers);
  }
});
```
↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓

```typescript
ask(commonQuestions).then((answers: inquirer.Answers) => {
  if (answers.hobbies == "Sports") {
    ask(sportsQuestion).then((sportsAnswers: inquirer.Answers) => {
      answers = { ...answers, ...sportsAnswers };
      console.log(answers);
    });
  } else {
    console.log(answers);
  }
});
```

次は、 `.then` を外してみる　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓

```typescript
const askAllQuestion = async (): Promise<inquirer.Answers> => {
  let answers = await ask(commonQuestions);
  if (answers.hobbies == "Sports") {
    const sportsAnswers = await ask(sportsQuestion);
    answers = { ...answers, ...sportsAnswers };
    // console.log(answers);
    return answers;
  } else {
    // console.log(answers);
    return answers;
  }
};

askAllQuestion().then((answers: inquirer.Answers) => {
  console.log(answers);
});
```

実行したら、[変更前](#実行-1)　と結果が同じになった 🙌

というわけで、Second Step 全体を実装。
>1. 趣味にスポーツを選択した場合には、どのスポーツが好きかの質問(選択式)をしたい
>2. 趣味にその他を選択した場合には、趣味を入力(記述式)してもらいたい

```typescript
import inquirer, { Answers } from "inquirer";

const commonQuestions: inquirer.QuestionCollection<inquirer.Answers> = [
  {
    type: "input",
    name: "name",
    message: "What is your name or nickname?",
  },
  {
    type: "checkbox",
    name: "hobbies",
    message: "Select your hobbies:",
    choices: ["Reading", "Traveling", "Cookiing", "Sports", "Music", "Others"],
  },
];

const sportsQuestion = [
  {
    type: "checkbox",
    name: "sports",
    message: "Select your favorite sports:",
    choices: [
      "Baseball",
      "Soccer",
      "Basketball",
      "Volleyball",
      "Badminton",
      "Tennis",
      "Others",
    ],
  },
];

const othersQuestion = [
  {
    type: "input",
    name: "other_hobbies",
    message: "What your hobbies?",
  },
];

const ask = async (
  questions: inquirer.QuestionCollection<inquirer.Answers>
): Promise<inquirer.Answers> => {
  const answers = await inquirer.prompt(questions);
  return answers;
};

const askAllQuestion = async (): Promise<inquirer.Answers> => {
  let answers = await ask(commonQuestions);
  if (answers.hobbies == "Sports") {
    const sportsAnswers = await ask(sportsQuestion);
    answers = { ...answers, ...sportsAnswers };
    return answers;
  } else if (answers.hobbies == "Others") {
    const othersAnswers = await ask(othersQuestion);
    answers = { ...answers, ...othersAnswers };
    return answers;
  } else {
    return answers;
  }
};

askAllQuestion().then((answers: inquirer.Answers) => {
  console.log(answers);
});
```

### 実行

```bash
mayumitakanoMBP:~/git/create_csv_from_cli npm start
> start
> ts-node ./src/main.ts

? What is your name or nickname? mayumi
? Select your hobbies: Sports
? Select your favorite sports: Volleyball
{ name: 'mayumi', hobbies: [ 'Sports' ], sports: [ 'Volleyball' ] }
mayumitakanoMBP:~/git/create_csv_from_cli npm start
> start
> ts-node ./src/main.ts

? What is your name or nickname? mayumi
? Select your hobbies: Others
? What your hobbies? making sweets
{
  name: 'mayumi',
  hobbies: [ 'Others' ],
  other_hobbies: 'making sweets'
}
```

あとは、この結果を利用して CSV 作成する部分が残っているけど、今回は async/await のことを残しておきたかったので、いったんここまで。

## まとめ
今までなんとなく動いたからとわかったつもりになっていた 非同期処理や async/await について、最終形にたどりつくまで少しずつ進めたので、自分なりに理解が深まった。
自分がやらかしがちなことなども残しておくことができたので、またつまづいた時には一から調べ直さなくてもここに戻ればよさそう。

## 参考にした情報

https://qiita.com/soarflat/items/1a9613e023200bbebcb3

https://www.cview.co.jp/cvcblog/2022.05.30.488_DPHMY7ikiNc7KhEO1

https://typescriptbook.jp/reference/functions/arrow-functions

https://dev.classmethod.jp/articles/inquirer-v9-to-v10-migration/