1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

asyncとawaitに触れてみた

Posted at

はじめに

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

やりたいこと

対話型CLIにて得た回答からCSVを作成する

環境構築

WindowsのWSLにて動作確認しています

1. volta のインストール

curl https://get.volta.sh | bash
volta --version

Node.js のバージョン管理ツール
今回作成するアプリは、OSが混じる可能性があるため、voltaを選択
他の選択肢でいうと、nodeenvnvm がある

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(対話型CLIライブラリ)のインストール

npm install inquirer

ドキュメントには、npm install --save inquirer と記載されていますが、npm 5.0.0 以降からは install 時にデフォルトで save してくれるので --save や -S オプションを指定する必要はない。

require('inquirer') がエラーになるので、ドキュメントにあるとおり、以下を実行

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

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

実装

First Step

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

code

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;
});

実行

途中経過

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

結果

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

うん、なんかよさげ。

Second Step

質問の回答によって、処理を分岐してみる

  1. 趣味にスポーツを選択した場合には、どのスポーツが好きかの質問(選択式)をしたい
  2. 趣味にその他を選択した場合には、趣味を入力(記述式)してもらいたい

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

code

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

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

つぶやき

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

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

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

うん、なんかよさげ。

いやでもさ...

つぶやき(再び)

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

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

async/await 使ってみる

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

const ask = async (
  questions: inquirer.QuestionCollection<inquirer.Answers>
): Promise<inquirer.Answers> => {
  const answers = await inquirer.prompt(questions);
  return answers;
};
ask関数作成の過程を見たい方はこちら

ask_function.gif

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

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

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);
  }
});

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

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 を外してみる ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

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);
});

実行したら、変更前 と結果が同じになった 🙌

というわけで、Second Step 全体を実装。

  1. 趣味にスポーツを選択した場合には、どのスポーツが好きかの質問(選択式)をしたい
  2. 趣味にその他を選択した場合には、趣味を入力(記述式)してもらいたい
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);
});

実行

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 について、最終形にたどりつくまで少しずつ進めたので、自分なりに理解が深まった。
自分がやらかしがちなことなども残しておくことができたので、またつまづいた時には一から調べ直さなくてもここに戻ればよさそう。

参考にした情報

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?