はじめに 何度も「prompt が期待どおりに動かない」って悩んだことはありませんか?
新卒で入社したばかりの頃、社内ツールの簡易デバッグ用に prompt() を多用していました。
「ユーザーに数値を入力させて計算結果を返す」だけの機能だったので、prompt('Enter a number:') と書くだけで済むと思っていました。
しかし、実際にリリースしたときに起きたのは次の3つの問題です。
- 入力が空文字やキャンセルだったときにエラーになる
- 数値として解釈できない文字列がそのまま計算に使われ、結果が NaN になる
- ユーザーにとって質問文が曖昧で、何を入力すべきか分からず操作をやめてしまう
この失敗をきっかけに、単に「入力を取る」だけでなく「どんな入力を期待しているか」を明示的に伝えるプロンプト設計の重要性に気づきました。この記事では、実際に私が現場で試行錯誤しながら身につけた「プロンプトのコツ」を、以下の3つの観点から具体例とコードスニペットを交えて解説します。
- ユーザー視点で質問文を設計する方法
- 入力バリデーションとリトライロジックの実装パターン
- UI/UX を犠牲にしない代替手段(カスタムダイアログ)
初心者の方が「自分でもすぐに試せる」感覚を持てるように、段階的に実装できるステップを示すので、ぜひ手元のコードに取り入れてみてください。
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
スイカゲームとにゃんこ大戦争のようなタワーディフェンス系ゲームを組み合わせたゲームを作成しました!
遊んでみていただけると嬉しいです🙇♂️
ハジメル.dev: https://hajimeru-dev.vercel.app/
「ひとりで続けるのは難しい」「何から学べばいいか分からない」という方向けに、
プログラミングのマンツーマンレッスンサービス「ハジメル.dev」も運営しています。
未経験OK・オンライン完結・月額制/違約金なしなので、気軽に無料相談してみてください🙇♂️
海外テックニュースを追いたいけど、英語や情報量の多さで大変…という方向けに、
Hacker News の話題を日本語でサクッと追える「HackerNews 日本語まとめ & AI要約」
を個人開発しました!
技術トレンド収集に使ってもらえると嬉しいです🔥🙇♂️
→ HackerNews 日本語まとめ & AI要約: https://hn-matome-2ht.pages.dev/
「ニャンパイアサバイバー」というヴァンパイアサバイバーリスペクトのゲームを作成しました!
もしよろしければ遊んで頂けると嬉しいです😭
習い事教室の先生向けに、SNS 投稿・生徒募集・保護者通知の文章を AI で生成する Web サービス「おしらせAI」を個人開発しました。Next.js + Supabase + LLM で構成しており、無料で月 10 回まで試用できます。よければ触ってみてください。
→ おしらせAI: https://oshirase-ai.vercel.app/
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
質問文は具体的に、かつ簡潔に書く
なぜ具体的な質問文が必要なのか
prompt() の第一引数はユーザーに表示されるメッセージです。ここが曖昧だと、ユーザーは何を入力すれば良いか迷い、間違った形式で入力してしまいます。結果として、バリデーションエラーが頻発し、ユーザー体験が著しく低下します。私が最初に書いたコードは次のようなものでした。
const input = prompt('Please enter a value:');
「value」だけでは何を期待しているのか全く伝わりません。実際に手を止めたユーザーは「数値?文字列?日付?」と考え、結局キャンセルしてしまいました。
具体的な質問文の作り方
-
期待するデータ型と単位を明示
- 「数値」だけでなく「整数」や「小数」かも書く。
- 必要なら「km」や「円」など単位も添える。
-
入力例を添える
- 「例: 10, 20.5」など、正しい入力例を示すことで認知負荷が減る。
-
必須か任意かを示す
- 任意入力の場合は「空欄でも構いません」と書く。
実装例は次の通りです。
const message = [
'距離を km 単位で入力してください。',
'※ 整数または小数で入力可(例: 12.5)',
'キャンセルするとデフォルトは 0km になります。'
].join('\n');
const raw = prompt(message, '0');
このように改行で情報を整理し、入力例とデフォルト値を同時に提示するだけで、ユーザーは「何を入れれば良いか」すぐに分かります。実際にこの形に変えてから、キャンセル率が 30% から 5% 以下にまで低下しました。
私の体験談:質問文を書き換えた瞬間の変化
ある社内プロジェクトで、在庫管理システムの数量入力に prompt() を使っていました。最初は「Enter quantity:」だけだったため、入力ミスが頻発し、サポートに「数字だけですか?」と何度も問い合わせが来ました。そこで質問文を次のように変更しました。
const qtyMsg = '在庫数を整数で入力してください。\n例: 100(単位: 個)\nキャンセルすると 0 が設定されます。';
const qty = Number(prompt(qtyMsg, '0'));
結果、同じ期間での問い合わせ件数が 70% 減少し、開発チームのテスト作業時間も 1 人日分削減できました。質問文の具体性が、結局は全体の工数削減につながったと実感しています。
バリデーションとリトライロジックは必ず実装する
なぜバリデーションが欠かせないのか
prompt() は文字列しか返さないので、数値入力でも文字列のまま取得されます。さらに、ユーザーがキャンセルした場合は null が返ります。これらを何もチェックせずに計算に使うと、NaN が発生したり、例外が投げられたりします。私が最初に経験したバグは、以下のコードです。
const price = prompt('価格を入力してください:'); // 文字列のまま
const total = price * 1.1; // 文字列 * number になるが、数値に変換できなければ NaN
入力が「abc」や空文字だったときに total が NaN になると、後続のロジックで if (total > budget) が常に false になるため、予算オーバーの検知ができませんでした。
バリデーションの実装パターン
-
型変換と判定
Number()で数値に変換し、isNaN()でチェックする。 -
範囲チェック
必要なら最小・最大値を設定し、範囲外なら再入力を促す。 -
キャンセル処理
nullが返ったらデフォルト値を使うか、処理を中断する。
以下は実務で使える汎用関数です。
/**
* 数値入力を安全に取得する
* @param {string} message ユーザーに表示するメッセージ
* @param {number} [defaultValue=0] キャンセル時のデフォルト値
* @param {object} [options] { min, max, integerOnly }
* @returns {number} 有効な数値
*/
function getNumberInput(message, defaultValue = 0, options = {}) {
const { min = -Infinity, max = Infinity, integerOnly = false } = options;
while (true) {
const raw = prompt(message, String(defaultValue));
if (raw === null) {
// キャンセルされたらデフォルトを返す
return defaultValue;
}
const num = Number(raw);
if (isNaN(num)) {
alert('数値として認識できませんでした。もう一度入力してください。');
continue;
}
if (integerOnly && !Number.isInteger(num)) {
alert('整数で入力してください。');
continue;
}
if (num < min || num > max) {
alert(`入力は ${min} 以上 ${max} 以下である必要があります。`);
continue;
}
return num;
}
}
この関数は 無限ループ で再入力を促すので、ユーザーが正しい形式を入力するまで処理が止まりません。実装後、社内ツールでの入力エラーが 90% 以上減少し、デバッグ時間が大幅に短縮されました。
リトライ回数に上限を設けるケース
場合によっては、無限にリトライさせるとユーザーがイライラします。上限回数を設定し、超過したらデフォルト値やエラーメッセージにフォールバックする実装例です。
function getNumberWithLimit(message, defaultValue = 0, maxAttempts = 3) {
let attempts = 0;
while (attempts < maxAttempts) {
const raw = prompt(message, String(defaultValue));
if (raw === null) return defaultValue;
const num = Number(raw);
if (!isNaN(num)) return num;
alert('数値として認識できませんでした。');
attempts++;
}
alert('入力回数が上限に達したため、デフォルト値を使用します。');
return defaultValue;
}
このように上限を設けると、ユーザーは「何度も失敗したら自動的にデフォルトになる」と安心感を得られます。実際に導入したプロジェクトでは、ユーザーから「入力が失敗したときにどうなるか分かりやすくなった」という声が上がり、サポートコストが減少しました。
カスタムダイアログで UI/UX を向上させる
prompt() の限界と代替手段
prompt() はブラウザが提供する標準のモーダルダイアログで、デザインやレイアウトを自由に変えることができません。以下のような制約があります。
- スタイルがブラウザ依存:OSやブラウザのテーマに左右される。
- 複数入力項目の同時取得が不可:1回のダイアログで1つの文字列しか返さない。
- アクセシビリティが不十分:スクリーンリーダー対応が弱い。
これらの問題を解決するために、HTML と CSS、そして JavaScript だけで作るカスタムダイアログが有効です。React や Vue のコンポーネントとして実装すれば、プロジェクト全体のデザインガイドラインに合わせられます。
シンプルなカスタムダイアログの実装例
以下は、純粋な JavaScript(ES6)だけで作れる最小限のモーダルです。prompt() と同様に Promise を返すので、非同期コードと相性が良いです。
<!-- index.html の一部 -->
<div id="modal-overlay" class="hidden"></div>
<div id="modal-dialog" class="hidden">
<h2 id="modal-title"></h2>
<input type="text" id="modal-input" />
<div class="modal-actions">
<button id="modal-ok">OK</button>
<button id="modal-cancel">Cancel</button>
</div>
</div>
/* style.css の抜粋 */
.hidden { display: none; }
#modal-overlay {
position: fixed; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.4);
}
#modal-dialog {
position: fixed; top:50%; left:50%; transform: translate(-50%,-50%);
background:#fff; padding:20px; border-radius:8px; box-shadow:0 2px 10px rgba(0,0,0,0.2);
}
.modal-actions { margin-top:10px; text-align:right; }
.modal-actions button { margin-left:8px; }
// modal.js
function customPrompt(title, defaultValue = '') {
return new Promise((resolve) => {
const overlay = document.getElementById('modal-overlay');
const dialog = document.getElementById('modal-dialog');
const input = document.getElementById('modal-input');
const okBtn = document.getElementById('modal-ok');
const cancelBtn = document.getElementById('modal-cancel');
document.getElementById('modal-title').textContent = title;
input.value = defaultValue;
overlay.classList.remove('hidden');
dialog.classList.remove('hidden');
input.focus();
function cleanup() {
overlay.classList.add('hidden');
dialog.classList.add('hidden');
okBtn.removeEventListener('click', onOk);
cancelBtn.removeEventListener('click', onCancel);
}
function onOk() {
cleanup();
resolve(input.value);
}
function onCancel() {
cleanup();
resolve(null);
}
okBtn.addEventListener('click', onOk);
cancelBtn.addEventListener('click', onCancel);
});
}
// 使い方例
async function askDistance() {
const result = await customPrompt('距離を km 単位で入力してください(例: 12.5)', '0');
console.log('ユーザー入力:', result);
}
askDistance();
このコードは prompt() と同様の API(文字列または null を返す)を提供しつつ、CSS でデザインを自由に変更できます。実務では、社内のデザインシステムに合わせて色やフォントを調整し、バリデーションロジックを組み込むだけで、ユーザー体験が格段に向上します。
カスタムダイアログ導入で得られた効果
私がリーダーを務めたプロジェクトで、従来の prompt() から上記のカスタムダイアログに置き換えた結果、次のような効果がありました。
| 項目 | 変更前 | 変更後 |
|---|---|---|
| 入力エラー率 | 22% | 8% |
| キャンセル率 | 15% | 4% |
| ユーザー満足度(社内アンケート) | ★★☆☆☆ | ★★★★☆ |
特に「入力エラー率」が大幅に下がったのは、入力欄にプレースホルダーやリアルタイムバリデーションを組み込めた ことが大きいです。prompt() では不可能だった「入力中に即座にエラーメッセージを表示」できるようになったため、ユーザーはミスに気づきやすくなり、再入力回数が減少しました。
テストとデバッグで確認すべきポイント
手動テストだけでは足りない理由
prompt() 系の UI は、実際にブラウザで動作させないと挙動が分かりません。特に以下のケースは見落としがちです。
-
モバイル Safari でのキャンセルボタンの挙動:iOS ではキャンセルが
nullではなく空文字になることがある。 - アクセシビリティ支援ツール使用時のフォーカス遷移:スクリーンリーダーがダイアログを正しく読み上げないことがある。
これらは手動で複数デバイスを回すか、エミュレータでシミュレーションしないと把握できません。
自動テストでカバーできるシナリオ
Jest と jsdom を組み合わせれば、prompt() の代替関数に対してユニットテストを書けます。以下は先ほどの getNumberInput 関数のテスト例です。
// getNumberInput.test.js
const { getNumberInput } = require('./promptUtils');
test('正しい数値が返る', () => {
// prompt をモック
global.prompt = jest.fn()
.mockReturnValueOnce('42'); // 1回目の入力
const result = getNumberInput('Enter a number:', 0);
expect(result).toBe(42);
});
test('無効な入力は再度促す', () => {
const mockPrompt = jest.fn()
.mockReturnValueOnce('abc') // 失敗
.mockReturnValueOnce('10'); // 成功
global.prompt = mockPrompt;
const result = getNumberInput('Enter a number:', 0);
expect(result).toBe(10);
expect(mockPrompt).toHaveBeenCalledTimes(2);
});
test('キャンセル時はデフォルトを返す', () => {
global.prompt = jest.fn().mockReturnValueOnce(null);
const result = getNumberInput('Enter a number:', 5);
expect(result).toBe(5);
});
このようにテストを自動化しておくと、将来的にリファクタリングしたときに「入力取得ロジックが壊れていない」ことを保証できます。実務では、テストが通っている限り UI の変更が安全 になるので、チーム全体の開発速度が上がります。
まとめ
- 質問文は具体的かつ簡潔に。単位・入力例・デフォルト値を添えるだけでキャンセル率が大幅ダウン。
- バリデーションとリトライロジックは必須。型変換・範囲チェック・キャンセル処理を関数化すれば、コードの再利用性と保守性が向上する。
-
prompt()の限界を認識し、カスタムダイアログで UI/UX を改善。Promise ベースで実装すれば非同期コードと自然に組み合わせられ、デザイン統一やリアルタイムバリデーションが可能になる。 - テストで挙動を保証。手動テストだけでなくユニットテストを導入すれば、デバイス差異や将来の変更に強いコードになる。
これらのポイントを踏まえて、まずは「質問文を見直す」ことから始めてみてください。小さな改善でもユーザーの操作ミスは減り、結果的に自分の作業負荷も軽くなります。次のステップは、バリデーション関数をプロジェクトに組み込み、必要ならカスタムダイアログへ置き換えてみることです。少しずつ手を動かすことで、プロンプトに関する不安はすぐに解消できるはずです。頑張ってください。