前提条件
偉大なる元ネタをはじめ、主な処理の流れはみなさん散々書いていらっしゃいますが、だいたいこんな感じ。
- 出力「契約書だよ。そこに名前を書きな。」
- ユーザからの入力を待つ
- 受け取った文字列からランダムに1文字抽出
- 出力「フン。
入力した文字列
というのかい。贅沢な名だねぇ。」 - 出力「今からお前の名前は
抽出した1文字
だ。いいかい、抽出した1文字
だよ。分かったら返事をするんだ、抽出した1文字
!!」
動作環境:
Google Chrome@最新版
node@v14.15.1
npm@6.14.8
typescript@4.1.2
※ 便宜上、Node.jsもECMAもひっくるめてJavaScriptと呼称します。
早速実装していこうと思います。
JavaScriptで湯婆婆を実装してみる
何はともあれ、超簡素なものを書いてみます。
const input = window.prompt('契約書だよ。そこに名前を書きな。');
const index = Math.floor(Math.random() * input.length);
const moji = input[index];
window.alert(`フン。${input}というのかい。贅沢な名だねぇ。`);
window.alert(`今からお前の名前は${moji}だ。いいかい、${moji}だよ。分かったら返事をするんだ、${moji}!!`);
このコードで登場する関数はすべてJavaScriptにあらかじめ定義されているものです。
うーん。いかにも、JavaScriptらしいコード面ですね。
なお、JavaScriptくんは良くも(?)悪くも開発者に寄り添いたい気持ちが強すぎて、よしなに解釈してくれてしまうので湯婆婆がクラッシュするなんてことが起こりません1。
フン。というのかい。贅沢な名だねぇ。
今からお前の名前はundefinedだ。いいかい、undefinedだよ。分かったら返事をするんだ、undefined!!
ただし、入力をせずにキャンセルボタンを押すかESCキーを押すと、window.prompt()
の返り値はnull
になって無事に湯婆婆はクラッシュします。
実にJavaScriptらしくて愛らしい。
細かい話
JavaScriptではwindow.prompt()
でユーザの入力をstring
型で同期的に受け取ることができます。
次に、受け取った値から無作為に1文字を抽出します。
string
型は配列に似ていて、input[0]
のように0番始まりで1文字ずつ参照できるので、入力された文字数未満の数をランダムに作ります。
Math.random()
は0〜1未満のランダムな数値を返します。
返り値は浮動小数点ですが、型は整数も小数点関係なくnumber
型です。
文字数は、string
型のインスタンスに生えているプロパティlength
に入っているので、Math.random() * 文字列.length
で、0〜文字数の範囲でランダムな数値を得ることができます。
最後にMath.floor
で小数点の切り捨てを行い、文字列のインデックス番号を用意します。
そこまでできたら、あとはwindow.alert()
で出力してフィニッシュです。
対戦ありがとうございました。
昨今のJavaScriptはブラウザの世界だけにとどまらない
みなさんご存知かと思われますが、JavaScriptにはNode.jsというつよつよな環境があります。
その環境ではブラウザで動かすようなJavaScriptのコードとほとんど同じものを動かせるのですが、window.prompt()
やwindow.alert()
などは…というよりwindow
オブジェクトがそもそもないので動作させることができません2。
そこで続いては、CLIで呼び出しても動くようにしてみたいと思います。
まずはNode.jsをインストールして、npm
コマンドが動くようにおきましょう(NodistなどでNode.jsをインストールしておいた方が便利かも)。
こんな感じのコードを書いて、「湯婆婆.js」という名前で保存します。
import prompts from 'prompts';
const {input} = await prompts({
type: 'text',
name: 'input',
message: '契約書だよ。そこに名前を書きな。',
});
const index = Math.floor(Math.random() * input.length);
const moji = input[index];
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${moji}だ。いいかい、${moji}だよ。分かったら返事をするんだ、${moji}!!`);
次に、そのjsファイルと同じ場所のpackage.jsonに次の内容を追記します(なければ作成します3)。
{
"type": "module",
"scripts": {
"start": "node 湯婆婆"
}
}
そして最後に、そのディレクトリで次のコマンドを打ちます。
npm i -save prompts
ここまでできたら準備完了、あとはnpm start
コマンドを叩くか、node 湯婆婆
コマンドを叩けば…
無事に動作しましたね。
細かい話
最初に書いたJSとの違いがいくつかあります。
まずはじめに、window.prompt()
の代わりにprompts
を利用している点です。
このprompts
というのは、Node.js用のパッケージ1つで、npmjs.comからnpm
コマンドを経由してダウンロードできます。
Node.jsではプロジェクトごとに使うパッケージをインストールしてから開発を進めます。
その作業がnpm i
というコマンドでした。
ダウンロードし終えたあとは、import
文を使ってパッケージ内のモジュールを読むことができます4。
import prompts from 'prompts';
const {input} = await prompts({
type: 'text',
name: 'input',
message: '契約書だよ。そこに名前を書きな。',
});
prompts
の振る舞いについては、promptsの仕様を参照してください。
window.prompt()
は同期的でしたが、こちらは非同期処理のためawait
キーワードで処理が終わるのを待っています。
後の流れは通常のJavascript同様で、最後の出力がconsole.log()
になっています5。
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${moji}だ。いいかい、${moji}だよ。分かったら返事をするんだ、${moji}!!`);
これは通常のブラウザで動くJavaScriptでも使うことができますが、通常はエンドユーザの目に触れるところに出力されません。
ブラウザでは開発者ツール6のConsoleタブで確認できます。
もっと汎用性を
値の受け取り方はどういう想定なのか?出力方法はどうしたいのか?
ブラウザで動かす想定なのか?Node.jSなのか?CLIで動かすのか?
シーンによって書き方が変化するのは、それはそうって感じですが、このままだとあまりにも汎用性がないですから、機能そのものを切り出してもっと呼び出しやすくしてみます。湯婆婆に汎用性がいるのかどうかは考えてはいけません。
湯婆婆で結局キモなのは
- 受け取った
string
から1文字を返す
この1点に尽きるので、そこを関数にしてしまいましょう。
const 湯婆婆 = input => value[Math.floor(Math.random() * value.length)];
…めっちゃ簡素になりましたね。
湯婆婆の本質は、長さを持つ値から無作為に1つ要素を返却することということがよくわかりました。
あとはこれを他のjsファイルから呼び出せる様にexport
文を書きます7。
/**
* 長さを持つ値から無作為に1つ要素を返す
* @param {string} 名前 - あなたの名前
* @returns {string} - あなたの新しい名前
*/
export const 湯婆婆 = 名前 => 名前[Math.floor(Math.random() * 名前.length)];
最後にJSDocを書いて、「湯婆婆.js」完成です。
いろんな方法で湯婆婆.jsを使ってみよう
ブラウザで使う
.
└── 湯婆婆
├── index.html
├── index.js
└── 湯婆婆.js
最初の方法
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>湯婆婆</title>
<script src="index.js" type="module"></script>
import {湯婆婆} from './湯婆婆.js';
const input = window.prompt('契約書だよ。そこに名前を書きな。');
const moji = 湯婆婆(input);
window.alert(`フン。${input}というのかい。贅沢な名だねぇ。`);
window.alert(`今からお前の名前は${moji}だ。いいかい、${moji}だよ。分かったら返事をするんだ、${moji}!!`);
DOMから値を読み取って、結果をDOMに表示する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>湯婆婆</title>
<script src="index.js" type="module"></script>
<style>
body {
text-align: center;
}
input {
margin-left: 5px;
}
</style>
</head>
<body>
<main>
<h1>契約書</h1>
<p>契約書だよ。そこに名前を書きな。</p>
<p><label for="name">あなたの名前</label><input type="text" id="name"></p>
<button id="ok">OK</button>
<output id="output"></output>
</main>
</body>
</html>
import {湯婆婆} from './湯婆婆.js';
const textField = document.getElementById('name');
const submitBtn = document.getElementById('ok');
const output = document.getElementById('output');
submitBtn?.addEventListener('click', () => {
const {value} = textField;
const word = 湯婆婆(value);
output.textContent = '';
output.insertAdjacentHTML('afterbegin', `
<p>フン。${value}というのかい。贅沢な名だねぇ。</p>
<p>今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!</p>
`);
});
Node.jsで動かす
.
└── 湯婆婆
├── package.json
├── index.js
└── 湯婆婆.js
{
"type": "module",
"scripts": {
"start": "node index"
},
"devDependencies": {
"prompts": "^2.4.0"
}
}
最初の方法
import prompts from 'prompts';
import {湯婆婆} from './湯婆婆.js';
const {input} = await prompts({
type: 'text',
name: 'input',
message: '契約書だよ。そこに名前を書きな。',
});
const word = 湯婆婆(input);
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);
悪用してArrayを投げてみる
import {湯婆婆} from './湯婆婆.js';
// 謎の呪文が詰まった配列(Array<string>)を渡してみる
const input = [
'ガタガタ',
'でたわね',
'げぼかわ',
'きtら',
'にーんじん',
'アーモンド',
];
const word = 湯婆婆(input);
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);
残念、JavaScriptらしく無事に動いてしまいました。
お客様とて許せぬ!
文字列から1文字だけを抽出する前提の湯婆婆。
配列を渡してくるのはやめていただきたいですね。
さて、JavaScriptの力を駆使して配列が飛んできたら例外を投げる処理を組み込んでやりましょう。
/**
* 長さを持つ値から無作為に1つ要素を返す
* @param {string} 名前 - あなたの名前
* @returns {string} - あなたの新しい名前
*/
export const 湯婆婆 = 名前 => {
if (typeof 名前 === 'string') {
return 名前[Math.floor(Math.random() * 名前.length)];
}
throw new TypeError('お客様とて許せぬ!');
}
これでどうでしょう。
はい、無事に湯婆婆は怒り狂ってクラッシュすることができました。
以上、湯婆婆.jsでした!
でもそれ、めんどくさくない?
いちいち受け取った値が正しいか検証するコードを書くなんて、Javascriptで使うライブラリやプラグインを作る意外では不毛な時間です。
引数や返り値の型が合っているか?なんてことはもう人間が考える必要はありません8。
JavaScriptとほとんど同じ記法で書けるTypeScriptを使いましょう。
まずは次のコマンドを実行します。
npm i -D typescript ts-node
tsconfig.jsonを作ります9。
{
"compilerOptions": {
"target": "ESNext",
"module": "commonjs",
"strict": true,
},
"include": [
"./**/*.ts"
]
}
package.jsonのコマンドを書き換えます。
{
"scripts": {
"start": "ts-node index",
},
"devDependencies": {
"prompts": "^2.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.1.2"
}
}
湯婆婆.jsは例外処理を書く前の状態にもどして、jsファイルの拡張子を2つともtsに書き換えます。
/**
* 長さを持つ値から無作為に1つ要素を返す
* @param {string} 名前 - あなたの名前
* @returns {string} - あなたの新しい名前
*/
export const 湯婆婆 = 名前 => 名前[Math.floor(Math.random() * 名前.length)];
import {湯婆婆} from './湯婆婆';
const input = [
'ガタガタ',
'でたわね',
'げぼかわ',
'きtら',
'にーんじん',
'アーモンド',
];
const word = 湯婆婆(input);
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);
最終的なフォルダ構成はこちら。
.
└── 湯婆婆
├── package.json
├── tsconfig.json
├── index.ts
└── 湯婆婆.ts
このまま、次のコマンドを叩くとどうなるでしょうか。
npm start
なんだかさっきと違うエラーがでましたね。
Parameter '名前' implicitly has an 'any' type.
これは「湯婆婆()
が受け取る関数の型がかかれてないぞ!」というエラーです。
TypeScriptは、どういう値を受け取るのかをあらかじめコードに書いておいて、コードをJavaScriptに変換してから実行するときに処理が合ってるか検査してくれます。
さっそく型を書いてみましょう。
/**
* 長さを持つ値から無作為に1つ要素を返す
* @param 名前 - あなたの名前
* @returns あなたの新しい名前
*/
export const 湯婆婆 = (名前: string) => 名前[Math.floor(Math.random() * 名前.length)];
// export const 湯婆婆 = (名前: string): string => 名前[Math.floor(Math.random() * 名前.length)]; 返り値も`string`だと明示するとこう。でもTypeScriptくんは賢いので書かなくてもわかるとこは書かなくてもいい
ついでに、JSDocもTSDocに変えてみました。これでどうでしょうか。
今度は別のことを怒られていますが、
Argument of type 'string[]' is not assignable to parameter of type 'string'.
これはstring
が渡されるはずなのにstring[]
(文字列だけが入った配列)が渡されてるぞ!約束と違う!と怒っているようです。
では、渡す値をstring
にしてみましょう。
import {湯婆婆} from './湯婆婆';
const input = '桐生ココ';
const word = 湯婆婆(input);
console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);
無事に動作しました!10
このように、自分が作った関数が正しく使われているかをいちいち気にしなくてよくなる点もTypeScriptの魅力ですが、記法が純粋なJavaScriptに追記するだけでというのも大きな魅力の一つです。JavaScriptが書ければとても入って行きやすい世界だと思います。
そうして規模が大きくなってくると型の受け渡しだけでなく、その関数や機能の処理自体が変わらないかをテストしたくなってきます。
自分が触った影響で他が壊れるかもしれない、という環境で作業をするのは精神的にもよくないので、人々はmocha, chai, jest, karmaなどなどキーワードにたどり着いてうんたらかんたら。
さいごに
なんだかTypeScriptの斡旋みたいな終わり方になってしまいましたが、TypeScriptにはちょびっと触れた程度なので、気になった方はぜひ調べてみてください。
TypeScriptは基本は最終的にJavaScriptに変換して使うものなので、
- tsconfig.jsonの設定
- ESlintの設定
- 変換する方法(
tsc
コマンド) - TypeScriptでどこまでチェックするのか
- jsファイルに変換したあとはBabelを通すのか
- モジュール同士はwebpackやSnowpackでまとめるのか
などなどのいろんな課題が立ちはだかってきますが、それは世の中に死ぬほど良記事がありますので、ぜひ探してみていただければと思います。
TypeScriptで書く意味あるの?と思っているみなさん、
JavaScriptでの湯婆婆実装はとてもコンパクトで簡単な話でしたが、それでも型チェックの必要性がすこしは見えたのではないでしょうか?
TypeScript基本だろjkのみなさん、マサカリは入り口においておいてもろて…><
結論、TSにしろJSにしろ、ECMAScript関連は書いててとても楽しいです。生まれてきてくれてありがとう。
そして、10日公開だったのに遅れてごめんなさい(現在11日午前3時40分)。
JavaScriptを知らない多言語プログラマさん向けに書こうと思ってたのですが、
書いてるあいだにわけわかんなくなってしまった感があります🙈
それでもここまで読んでくださった皆さん、ありがとうございました!!
-
JavaScriptにはJavaScript特有の
undefined
という、null
ではないがnull
みたいな型が存在しますが、これを文字列と結合させようとすると暗黙の型変換が起こり、undefined
型の値はundefined
というstring
型になってしまいます。 ↩ -
Node.jsでは
global
という名前のグローバル変数があり、JavaScriptでwindow
に生えているメソッドやプロパティの多くは、global
にも生えています ↩ -
通常、一番最初は
npm init
でpackage.json
を作成してから開発を進めます。npm init -y
で諸々設定するのを省略することともできます。 ↩ -
ESMはモダンブラウザだと動作しますが、Node.jsで
import
文が使えるのはpackage.json
にtype: module
が記されているか、拡張子が.mjs
のもののみです(v14.15.1現在)。それ以外でパッケージを読み込むにはrequire()
を用います。 ↩ -
Google ChromeならF12キーや
Cmd + Option + I
(Mac)、Ctrl+Shift+I
(Win)で開くことができます ↩ -
ESMはモダンブラウザだと動作しますが、Node.jsで
export
文が使えるのはpackage.json
にtype: module
が記されているか、拡張子が.mjs
のもののみです(v14.15.1現在)。それ以外でパッケージを読み込むにはmodule.exports
を用います。 ↩ -
語弊がありますが、多くの場合はTSの型推論とチェックにまかせていればそこまで神経質になる必要はない気がしています。 ↩
-
この設定は適当です。とりあえず動かすための設定なので、実際にプロジェクトでTypeScriptを採用するときはよく考えて設定してください。他にもたくさんの設定項目があります。 ↩
-
TypeScript対応していないパッケージは読めなかったり、動かすためには型ファイルもインストールしなかったりいけないケースもあるので注意が必要です。今回の例ではESM式のモジュールの読み込みをしていますが、TypeScriptかどうかにかかわらずともESMに対応してないパッケージをCommonJS方式で読み込むことや、その逆をしようとすると動作しなかったりするので、いきなり導入に踏み切るよりその辺りも含めてある程度理解してからプロジェクトに持ち込むほうが安全です。 ↩