LoginSignup
9
2

More than 3 years have passed since last update.

湯婆婆.js 湯婆婆.ts

Last updated at Posted at 2020-12-10

前提条件

偉大なる元ネタをはじめ、主な処理の流れはみなさん散々書いていらっしゃいますが、だいたいこんな感じ。

  1. 出力「契約書だよ。そこに名前を書きな。」
  2. ユーザからの入力を待つ
  3. 受け取った文字列からランダムに1文字抽出
  4. 出力「フン。入力した文字列というのかい。贅沢な名だねぇ。」
  5. 出力「今からお前の名前は抽出した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になって無事に湯婆婆はクラッシュします。

image.png

実に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」という名前で保存します。

湯婆婆.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)。

package.json
{
  "type": "module",
  "scripts": {
    "start": "node 湯婆婆"
  }
}

そして最後に、そのディレクトリで次のコマンドを打ちます。

npm i -save prompts 

ここまでできたら準備完了、あとはnpm startコマンドを叩くか、node 湯婆婆コマンドを叩けば…

image.png

無事に動作しましたね。

細かい話

最初に書いた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

湯婆婆.js
/**
 * 長さを持つ値から無作為に1つ要素を返す
 * @param {string} 名前 - あなたの名前
 * @returns {string} - あなたの新しい名前
 */
export const 湯婆婆 = 名前 => 名前[Math.floor(Math.random() * 名前.length)];

最後にJSDocを書いて、「湯婆婆.js」完成です。

いろんな方法で湯婆婆.jsを使ってみよう

ブラウザで使う

ファイル構成
.
└── 湯婆婆
    ├── index.html
    ├── index.js
    └── 湯婆婆.js

最初の方法

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>湯婆婆</title>
<script src="index.js" type="module"></script>
index.js
import {湯婆婆} from './湯婆婆.js';

const input = window.prompt('契約書だよ。そこに名前を書きな。');
const moji = 湯婆婆(input);

window.alert(`フン。${input}というのかい。贅沢な名だねぇ。`);
window.alert(`今からお前の名前は${moji}だ。いいかい、${moji}だよ。分かったら返事をするんだ、${moji}!!`);

DOMから値を読み取って、結果をDOMに表示する

index.html
<!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>
index.js
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
package.json(startコマンドが変わってます)
{
  "type": "module",
  "scripts": {
    "start": "node index"
  },
  "devDependencies": {
    "prompts": "^2.4.0"
  }
}

最初の方法

index.js
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を投げてみる

index.js
import {湯婆婆} from './湯婆婆.js';

// 謎の呪文が詰まった配列(Array<string>)を渡してみる
const input = [
  'ガタガタ',
  'でたわね',
  'げぼかわ',
  'きtら',
  'にーんじん',
  'アーモンド',
];
const word = 湯婆婆(input);

console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);

image.png

残念、JavaScriptらしく無事に動いてしまいました。

お客様とて許せぬ!

文字列から1文字だけを抽出する前提の湯婆婆。
配列を渡してくるのはやめていただきたいですね。

さて、JavaScriptの力を駆使して配列が飛んできたら例外を投げる処理を組み込んでやりましょう。

湯婆婆.js
/**
 * 長さを持つ値から無作為に1つ要素を返す
 * @param {string} 名前 - あなたの名前
 * @returns {string} - あなたの新しい名前
 */
export const 湯婆婆 = 名前 => {
  if (typeof 名前 === 'string') {
    return 名前[Math.floor(Math.random() * 名前.length)];
  }

  throw new TypeError('お客様とて許せぬ!');
}

これでどうでしょう。

image.png

はい、無事に湯婆婆は怒り狂ってクラッシュすることができました。

以上、湯婆婆.jsでした!

でもそれ、めんどくさくない?

いちいち受け取った値が正しいか検証するコードを書くなんて、Javascriptで使うライブラリやプラグインを作る意外では不毛な時間です。
引数や返り値の型が合っているか?なんてことはもう人間が考える必要はありません8

JavaScriptとほとんど同じ記法で書けるTypeScriptを使いましょう。

まずは次のコマンドを実行します。

npm i -D typescript ts-node

tsconfig.jsonを作ります9

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "strict": true,
  },
  "include": [
    "./**/*.ts"
  ]
}

package.jsonのコマンドを書き換えます。

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に書き換えます。

湯婆婆.ts
/**
 * 長さを持つ値から無作為に1つ要素を返す
 * @param {string} 名前 - あなたの名前
 * @returns {string} - あなたの新しい名前
 */
export const 湯婆婆 = 名前 => 名前[Math.floor(Math.random() * 名前.length)];
index.ts
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

image.png

なんだかさっきと違うエラーがでましたね。

Parameter '名前' implicitly has an 'any' type.

これは「湯婆婆()が受け取る関数の型がかかれてないぞ!」というエラーです。

TypeScriptは、どういう値を受け取るのかをあらかじめコードに書いておいて、コードをJavaScriptに変換してから実行するときに処理が合ってるか検査してくれます。
さっそく型を書いてみましょう。

湯婆婆.ts
/**
 * 長さを持つ値から無作為に1つ要素を返す
 * @param 名前 - あなたの名前
 * @returns あなたの新しい名前
 */
export const 湯婆婆 = (名前: string) => 名前[Math.floor(Math.random() * 名前.length)];
// export const 湯婆婆 = (名前: string): string => 名前[Math.floor(Math.random() * 名前.length)]; 返り値も`string`だと明示するとこう。でもTypeScriptくんは賢いので書かなくてもわかるとこは書かなくてもいい

ついでに、JSDocもTSDocに変えてみました。これでどうでしょうか。

image.png

今度は別のことを怒られていますが、

Argument of type 'string[]' is not assignable to parameter of type 'string'.

これはstringが渡されるはずなのにstring[](文字列だけが入った配列)が渡されてるぞ!約束と違う!と怒っているようです。

では、渡す値をstringにしてみましょう。

index.ts
import {湯婆婆} from './湯婆婆';

const input = '桐生ココ';
const word = 湯婆婆(input);

console.log(`フン。${input}というのかい。贅沢な名だねぇ。`);
console.log(`今からお前の名前は${word}だ。いいかい、${word}だよ。分かったら返事をするんだ、${word}!!`);

image.png

無事に動作しました!10

このように、自分が作った関数が正しく使われているかをいちいち気にしなくてよくなる点もTypeScriptの魅力ですが、記法が純粋なJavaScriptに追記するだけでというのも大きな魅力の一つです。JavaScriptが書ければとても入って行きやすい世界だと思います。

そうして規模が大きくなってくると型の受け渡しだけでなく、その関数や機能の処理自体が変わらないかをテストしたくなってきます。
自分が触った影響で他が壊れるかもしれない、という環境で作業をするのは精神的にもよくないので、人々はmocha, chai, jest, karmaなどなどキーワードにたどり着いてうんたらかんたら。

さいごに

なんだかTypeScriptの斡旋みたいな終わり方になってしまいましたが、TypeScriptにはちょびっと触れた程度なので、気になった方はぜひ調べてみてください。
TypeScriptは基本は最終的にJavaScriptに変換して使うものなので、

  • tsconfig.jsonの設定
  • ESlintの設定
  • 変換する方法(tscコマンド)
  • TypeScriptでどこまでチェックするのか
  • jsファイルに変換したあとはBabelを通すのか
  • モジュール同士はwebpackSnowpackでまとめるのか

などなどのいろんな課題が立ちはだかってきますが、それは世の中に死ぬほど良記事がありますので、ぜひ探してみていただければと思います。

TypeScriptで書く意味あるの?と思っているみなさん、
JavaScriptでの湯婆婆実装はとてもコンパクトで簡単な話でしたが、それでも型チェックの必要性がすこしは見えたのではないでしょうか?

TypeScript基本だろjkのみなさん、マサカリは入り口においておいてもろて…><

結論、TSにしろJSにしろ、ECMAScript関連は書いててとても楽しいです。生まれてきてくれてありがとう。

そして、10日公開だったのに遅れてごめんなさい(現在11日午前3時40分)。

JavaScriptを知らない多言語プログラマさん向けに書こうと思ってたのですが、
書いてるあいだにわけわかんなくなってしまった感があります🙈

それでもここまで読んでくださった皆さん、ありがとうございました!!


  1. JavaScriptにはJavaScript特有のundefinedという、nullではないがnullみたいな型が存在しますが、これを文字列と結合させようとすると暗黙の型変換が起こり、undefined型の値はundefinedというstring型になってしまいます。 

  2. Node.jsではglobalという名前のグローバル変数があり、JavaScriptでwindowに生えているメソッドやプロパティの多くは、globalにも生えています 

  3. 通常、一番最初はnpm initpackage.jsonを作成してから開発を進めます。npm init -yで諸々設定するのを省略することともできます。 

  4. ESMはモダンブラウザだと動作しますが、Node.jsでimport文が使えるのはpackage.jsontype: moduleが記されているか、拡張子が.mjsのもののみです(v14.15.1現在)。それ以外でパッケージを読み込むにはrequire()を用います。 

  5. consoleにはいろんな種類があるのでぜひ試してみてください 

  6. Google ChromeならF12キーやCmd + Option + I(Mac)、Ctrl+Shift+I(Win)で開くことができます 

  7. ESMはモダンブラウザだと動作しますが、Node.jsでexport文が使えるのはpackage.jsontype: moduleが記されているか、拡張子が.mjsのもののみです(v14.15.1現在)。それ以外でパッケージを読み込むにはmodule.exportsを用います。 

  8. 語弊がありますが、多くの場合はTSの型推論とチェックにまかせていればそこまで神経質になる必要はない気がしています。 

  9. この設定は適当です。とりあえず動かすための設定なので、実際にプロジェクトでTypeScriptを採用するときはよく考えて設定してください。他にもたくさんの設定項目があります。 

  10. TypeScript対応していないパッケージは読めなかったり、動かすためには型ファイルもインストールしなかったりいけないケースもあるので注意が必要です。今回の例ではESM式のモジュールの読み込みをしていますが、TypeScriptかどうかにかかわらずともESMに対応してないパッケージをCommonJS方式で読み込むことや、その逆をしようとすると動作しなかったりするので、いきなり導入に踏み切るよりその辺りも含めてある程度理解してからプロジェクトに持ち込むほうが安全です。 

9
2
6

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
9
2