くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

Web ナイト宮崎 Vol.10 ~てげ TypeScript を学びたい~ で登壇しました

f:id:curama-tech:20201009174601p:plain

こんにちは、バックエンドエンジニアの @akira です。

9 月 18 日に宮崎で開催された勉強会【Web ナイト宮崎 Vol.10 ~てげ TypeScript を学びたい~】で「TypeScript の列挙型について」というテーマで発表してきました!

今回はその内容を記事として再編成・加筆したものになります。


はじめに

今回は 「TypeScript Enum 〜使いどころの難しい Enum ??〜」 というテーマで発表させて頂きます。

発表内容は以下の二点です。

  • TypeScript Enum の基礎
  • 複雑な Enum の表現方法

Agenda は次のようになっています。

  • Numeric Enum
  • Union of Literal Type
  • String Enum
  • Const Enum
  • Namespace
  • Enum Class
  • 制約まとめ

この後提示するコードは全て TypeScript Playground v4.0.2 にて確認しています。

それでは早速 Numeric Enum から見ていきましょう。

Numeric Enum

Numeric Enum とは number 型の値をもつ Enum です。

enum Fruits {
  Apple,
  Orange,
  Lemon,
}

let fruit = Fruits.Apple;
console.log(fruit); // 0
fruit = 1; // number is also acceptable
fruit = 4; // not error

number 型の値なら代入できてしまうため、型安全ではありません。
Enum で指定した値以外の number も代入が可能です。

string 型は代入できません。

fruit = "Apple"; // Error: Type '"Apple"' is not assignable to type 'Fruits'.

トランスパイルされた JavaScript を見てみます。

// JS
var Fruits;
(function (Fruits) {
  Fruits[(Fruits["Apple"] = 0)] = "Apple";
  Fruits[(Fruits["Orange"] = 1)] = "Orange";
  Fruits[(Fruits["Lemon"] = 2)] = "Lemon";
})(Fruits || (Fruits = {}));

Fruits["Apple"] = 0 は 0 を返し(reverse mapping)、Value と Key のどちらも指定が可能です。
範囲外の値を指定した場合は undefined が返されます。

console.log(Fruits[0]); // "Apple"
console.log(Fruits["Apple"]); // 0
console.log(Fruits.Apple); // 0
console.log(Fruits[10]); // undefined

また、値の初期値は明示的に指定可能です。

enum Fruits {
  Apple = 1,
  Orange, // 2
  Lemon, // 3
}

console.log(Fruits.Orange); // 2

Union of Literal Type

リテラル型の Union 型は指定した値以外は代入できないため、Numeric Enum に代わる良い選択肢となるでしょう。

const Fruit = {
  Apple: 0,
  Orange: 1,
  Lemon: 2,
} as const;
type Fruit = typeof Fruit[keyof typeof Fruit]; // 0 | 1 | 2

let f: Fruit;
f = 0; // OK
f = Fruit.Lemon; // OK
f = 4; // Error: Type '4' is not assignable to type 'Fruit'.

トランスパイルされた JavaScript はとてもシンプルです。

// JS
const Fruit = {
  Apple: 0,
  Orange: 1,
  Lemon: 2,
};

String Enum

続いて String Enum です。
String Enum は string 型の値をもつ Enum ですが、string 型自体は代入できないため、型安全です。

enum Fruits {
  Apple = "Apple",
  Orange = "Orange",
  Lemon = "Lemon",
}

let fruit = Fruits.Apple;
fruit = "Orange"; // Error: Type '"Orange"' is not assignable to type 'Fruits'

トランスパイルされた JavaScript を見てみます。

// JS
var Fruits;
(function (Fruits) {
  Fruits["Apple"] = "Apple";
  Fruits["Orange"] = "Orange";
  Fruits["Lemon"] = "Lemon";
})(Fruits || (Fruits = {}));

reverse mapping もされておらず、Key のみ指定可能です。

enum Fruits {
  Apple = "Apple",
  Orange = "Orange",
  Lemon = "Lemon",
}

console.log(Fruits["Apple"]); // "Apple"
console.log(Fruits.Apple); // "Apple"

Value を Key と同じにすることで、可読性が上がり、debug しやすいのではないでしょうか。

Const Enum

次は Const Enum です。

Const Enum は、Numeric Enumconst キーワードが付与された Enum です。

const enum Fruits {
  Apple,
  Orange,
  Lemon,
}

Const Enum の特徴を説明する前に、先程の Numeric Enum のトランスパイル後の JavaScript をもう一度見てみましょう。

enum Fruits {
  Apple,
  Orange,
  Lemon,
}

const apple = Fruits.Apple;
// JS
var Fruits;
(function (Fruits) {
  Fruits[(Fruits["Apple"] = 0)] = "Apple";
  Fruits[(Fruits["Orange"] = 1)] = "Orange";
  Fruits[(Fruits["Lemon"] = 2)] = "Lemon";
})(Fruits || (Fruits = {}));

const apple = Fruits.Apple;

上記を見ると、即時関数によってオブジェクトを変数に代入していることがわかります。

Const Enum を使った以下のコードの、トランスパイル後の JavaScript を見てみます。

const enum Fruits {
  Apple,
  Orange,
  Lemon,
}

const apple = Fruits.Apple;
// JS
const apple = 0; /* Apple */

Numeric Enum と比べ、値がインライン化されていることがわかります。よってパフォーマンスの向上が期待できます。

ただし、Const Enum も Numeric Enum 同様、型安全ではありません。

const enum Fruits {
  Apple,
  Orange,
  Lemon,
}

let fruit = Fruits.Apple;
fruit = 1; // number is also acceptable
fruit = 10; // not error

Namespace

Namespace を使うと、Enum にメソッドを追加することができます。

enum Fruits {
  Apple,
  Orange,
  Lemon,
}

namespace Fruits {
  export function isApple(fruit: Fruits): boolean {
    switch (fruit) {
      case Fruits.Apple:
        return true;
      default:
        return false;
    }
  }
}

ただし、Const Enum には Namespace を定義できません。

const enum Fruits { // Error: Enum declarations can only merge with namespace or other enum declarations.
  Apple,
  Orange,
  Lemon,
}

// Error: Enum declarations can only merge with namespace or other enum declarations.
namespace Fruits {
  export function isApple(fruit: Fruits): boolean {
    switch (fruit) {
      case Fruits.Apple:
        return true;
      default:
        return false;
    }
  }
}

Enum Class

ここまで一通り Enum を見てきましたが、Numeric Enum や Const Enum には制約があったかと思います。
メソッドを持つ少し複雑な Enum を表現したい場合、弊社では Enum 用の Class を定義しています。

まず Enum の抽象クラスを定義します。

abstract class Enum<T> {
  constructor(readonly value: T) {}

  public is(e: Enum<T>): boolean {
    return this.value === e.value;
  }

  public isNot(e: Enum<T>): boolean {
    return !this.is(e);
  }
}

続いて具象クラスを String Enum と共に定義します。

enum FruitNames {
  Apple = "Apple",
  Orange = "Orange",
}

class Fruits extends Enum<string> {
  public static [FruitNames.Apple]: Fruits = new Fruits(FruitNames.Apple, 100, 0.5);
  public static [FruitNames.Orange]: Fruits = new Fruits(FruitNames.Orange, 200, 1);

  constructor(value: string, readonly price: number, readonly weight: number) {
    super(value);
  }
}

定数は String Enum に、ロジックは Class に定義します。
また、Static Field として各オブジェクトを保持することで Enum のように扱えます。

上記の例では、りんごやオレンジなどの果物に対し、料金と重さの紐付けを表現しています。

クライアントコードは次のようになります。

let fruit = Fruits.Orange;
if (fruit.is(Fruits.Orange)) {
  console.log(fruit.price, fruit.weight);
}
fruit = "Orange"; // Error: Type 'string' is not assignable to type 'Fruits'.

上記のように、string 型の値は代入できないため、型安全です。

制約まとめ

それぞれ詳細を見てきましたが、制約をまとめると以下になります。

範囲外の値を代入できない 型安全
Numeric Enum
Union of Literal Type
String Enum
Const Enum
Enum Class

最後に

今回のまとめです。

  • Enum は万能ではない
  • Union of Literal Type も良い選択肢である
  • 複雑な Enum は Class を使うのがおすすめ

以上になります。ありがとうございました!

Ansible の variable register は when でスキップできない

f:id:curama-tech:20200930155627p:plain

こんにちは。 バックエンドエンジニア / SRE のまのめです。

くらしのマーケットのデプロイには、 Ansible が採用されています。
Ansible では、実行したコマンドの結果などを変数に入れる register というキーワードがあります。
小ネタですが、この register で本番デプロイ時にハマったので、そのことを書いていきます。

問題の Task

やりたかったことは、以下のような処理です。

もし env が prod なら本番環境用の 設定値 を、
そうではなく env が kaizen なら 改善環境用の 設定値 を
AWS の Parameter Store から取ってきて、register する

これを愚直に playbook に起こすと、以下のようになります。

- name: Set Prod Config
  command: aws ssm get-parameters --region ap-northeast-1 --name prod.config --query Parameters[0].Value --output text
  register: config
  when: "env == 'prod'"

- name: Set Kaizen Config
  command: aws ssm get-parameters --region ap-northeast-1 --name kaizen.config --query Parameters[0].Value --output text
  register: config
  when: "env == 'kaizen'"

- name: Print Result
  command: "echo '{{ config.stdout }}'"

※ Print している箇所は、実際は template に render するなどしています。

さて、結果はどうなるでしょうか。

  • 改善環境( env = 'kaizen' ): 改善環境用の設定値がセットされる
  • 本番環境( env = 'prod' ): dict object has no attribute stdout という エラー

本番デプロイ中だというのに、困りました。

原因

先程の例で、 env = 'prod' のとき、config に register されていたものは、以下のような dict です。

"msg": {
    "changed": false, 
    "skip_reason": "Conditional result was False", 
    "skipped": true
}

condition が false になるパターンは env == 'kaizen' の場合ですね。
register がスキップされたのではなく、「評価されて false になった」という内容が register されて上書きされてしまっていました。

つまり記事タイトルのとおりですが、Ansible の register は when によってスキップできないようです。
ドキュメントにも書いてありました。

If a task fails or is skipped, Ansible still registers a variable with a failure or skipped status, unless the task is skipped based on tags.

Using Variables / Ansible Documentation

回避策

この箇所をコメントアウトし、ファイルに直書きしたものを読み込む方式に切り替えました。
本当は、「値が更新されても 1 箇所変えれば全てに適用されるようにしたい」という意図で Parameter Store から取る方式にしていたので、ちょっと不本意な形です。
何かいい方法はないのでしょうか……もし知見がある方がいらっしゃいましたら、教えていただきたいです。

最後に

こういう小ネタも案外ドキュメントにちゃんと書いてあることもあるので、しっかりと目を通しておかないといけないですね。
なお、結局未だにいい解決方法が確立できていません。
くらしのマーケットは、まだまだデプロイ周りでもこういった改善ができるところがたくさんあります。
SRE として一緒に育ててくださる方は、ぜひ こちら からお気軽にご連絡ください!

TypeScriptを使ってDiscord botを作ってみよう!

はじめに

こんにちは!今年新卒入社しました、エンジニアのタナカです。

早速ですが、ここ最近リモートワークが推奨されている中、「Discord(ディスコード)」というツールを導入した、もしくは導入してはいないけれども単語は聞いたことがあるという方が増えているのではないでしょうか。

このツールは過去に 「ゲーマー向けチャットツール」 とよく言われてましたが、2020年7月にブランディングイメージが 「あらゆるコミュニティが使えるコミュニケーションツール」 に変更され、ゲームだけに留まらないツールとなりそうです。

音声も結構クリーンな感じなのもまたいいところですよね👍

さて、この「Discord」というツールなのですが…実はこれ、チャットや会話をするだけではなく、botだって作れるんです。

私も実際にbotを作ってみましたが、とても楽しく作れます!

「プログラミング一通り学んだけど次何したら良いか分かんない…」という方にもきっとオススメできます。

なので今回は、TypeScriptでのDiscord botの作り方(コード部分)について書いていきたいと思います!

※本記事は9/18(金)に協賛枠で参加したWebナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~にて Tunakan という名義で発表した内容から一部を抜粋し、ブログ用に編集したものになります。

 

Discord bot 作ってみよう!

準備

事前に必要なものは以下になります。

…え?これだけ?

そうです。これだけで作れちゃいます!

24時間フル稼働させるような規模のものでなければ、たったの3つで出来ます!

ちなみに今回JavaScriptではなくTypeScriptを使う理由としては、くらしのマーケットで使われている言語の1つなので使い慣れているという点があります。

そして以下が作成したサーバーにbotを追加した状態になります。

f:id:curama-tech:20200924134126p:plain

右側に 「Tunakan bot というのがオフラインで存在してますね。こちらが今回準備したbotになります。

…おや?何やらDiscord画面の下の方に面白い言葉が書いてありますね?

f:id:curama-tech:20200924134221p:plain

「ご挨拶しな!」と半ば強引に挨拶を求められてますね…😅

ただこの状態だと(作成してすぐなので当然ですが)何も返してくれません。シカトされちゃいます。悲しいですね…

なので、まずは簡単に挨拶出来る機能を追加しちゃいましょう!💪

botが挨拶出来るようにしよう!

最初に、discord.jsというNode.jsモジュールをインストールします。MacLinux系ならターミナル、Windowsならコマンドプロンプト、その他お好きなコマンドラインツールがあればそちらを開き以下を実行しましょう!

npm install discord.js

次にテキストエディタを開いて以下のコードをサクッと書いて保存してください。

// ./bot.ts

import { Client, Message } from "discord.js";
import { config } from "./config";

const client = new Client();

interface ReplyInterface {
    messageReply(message: Message): Promise<void>;
}

class Reply implements ReplyInterface {
    public async messageReply(message: Message): Promise<void> {
        if (message.author.bot) {
            return;
        } else if (message.content === "こんにちは") {
            message.reply("こんにちは!");
        }
    }
}

const reply = new Reply();

client.on("message", (message) => reply.messageReply(message));

client.login(config.token);
// ./config.ts

export const config = {
    token: "{token}",   // Developer Portalからtokenをコピペする
};

ファイル構成は以下のようになるはずです。

f:id:curama-tech:20200924134244p:plain

このサクッと書いたコードですが、以下の流れになります

  1. client.loginbotがオンライン状態になる
  2. 何かしらのメッセージが送信されたらreply.messageReply(message)を実行
  3. if (message.author.bot) { return; }で、メッセージがbotからならreturn
  4. bot以外で"こんにちは"というメッセージが送信されたら"こんにちは!"と発言者に対してリプライをする

bot作るなら結構複雑なコード書かなきゃいけないんじゃ…?と思われそうですが(実際私もそう思っていました)、単純なものならこの通りたった数行で作れます!👍

では実際に動かしてみましょう!以下をコマンドラインツールで実行してみてください!

.tsファイルのコンパイルを行った後、botを起動します!

tsc bot.ts
node bot.js

f:id:curama-tech:20200924134306p:plain

f:id:curama-tech:20200924134320p:plain

うまく行けばこの通り、Tunakan botがオンライン状態になり、挨拶をしてくれます!

挨拶だけじゃ物足りない!もっと凝ったものを作る

さて、挨拶するだけじゃかなり物足りないですよね?

せっかく簡単に作れるんだから、何かもうちょっと凝ったものを作りたい…少し手軽にbotらしく動作させたい…うーん…

そうだ、じゃんけんゲームを作ろう!

というわけでじゃんけんのコードをサクッと書いちゃいましょう。

import { Collection } from "@discordjs/collection";
import { Client, Message } from "discord.js";
import { config } from "./config";

const client = new Client();

enum hands {
    "グー",
    "チョキ",
    "パー",
}

interface ReplyInterface {
    messageReply(message: Message): Promise<void>;
}

interface GameInterface {
    playRockPaperScissorsGame(message: Message): Promise<void>;
}

class Reply implements ReplyInterface {
    private game = new Game();

    public async messageReply(message: Message): Promise<void> {
        if (message.content === "じゃんけん") {
            await this.game.playRockPaperScissorsGame(message);
        }
    }
}

class Game implements GameInterface {
    public async playRockPaperScissorsGame(message: Message): Promise<void> {
        await message.reply("最初はグー!じゃんけん…!");
        const filter = (player) => {
            return ["グー", "チョキ", "パー"].includes(player.content);
        };

        let player: Collection<string, Message>;
        try {
            player = await message.channel.awaitMessages(filter, { max: 1, time: 10000, errors: ["time"] });
        } catch {
            message.reply("タイムオーバー!あなたの負けです");
            return;
        }

        if (!player) {
            // playerがundefinedならログアウトしてエラーを出す
            client.destroy();
            throw new Error("player is undefined");
        }

        // botの手を決める
        const botHand = Math.floor(Math.random() * 3);
        await message.reply(hands[botHand]);

        // 判定を行う
        const judge: number = (hands[player.first().content] - botHand + 3) % 3;
        switch (judge) {
            case 0:
                await message.reply("あいこ");
                break;
            case 1:
                await message.reply("あなたの負け");
                break;
            case 2:
                await message.reply("あなたの勝ち");
                break;
            default:
                await message.reply("Error!");
                break;
        }
    }
}

const reply = new Reply();
client.on("message", (message) => reply.messageReply(message));
client.login(config.token);

処理の流れは以下の通りです。サクッと作成したものなので、あいこでも再度勝負をしない一発勝負という点にご注意ください。

  1. ユーザーが「じゃんけん」とメッセージを送るとplayRockPaperScissorsGame()を実行
  2. 「最初はグー!じゃんけん…!」とreplyし、ユーザーからのメッセージをawaitMessagesで待つ
  3. filterで設定した文字列「グー」「チョキ」「パー」のメッセージが送信された時、bot側の手をランダムで決め、botの手をreplyする
  4. 計算してじゃんけんの判定を行い、それぞれの結果をreplyする

また、じゃんけんの判定ロジックは以下になります

  1. グー(0) チョキ(1) パー(2)とする
  2. ( {プレイヤーの手} - {botの手} + 3 ) % 3 をする
  3. 計算結果が0ならあいこ、1なら負け、2なら勝ちになる

コードが書けたらbotを起動してみましょう!tsc bot.tsコンパイルも忘れずに!

f:id:curama-tech:20200924134337p:plain

さいごに

Discordのbotは気軽に作れて、プログラミングの勉強にもなると思います!

Discordを使っている、サーバー持ってたりしててもうちょっといい感じにしたい!楽に運用したい!って時や、TypeScriptを勉強したい…!って時に作ってみるのもいいかもしれませんね👍

ログイン画面の「当たり前」を調査

みんなのマーケットでUI/UXデザイナーをしているミソサクです。

モバイルデザインの「当たり前」の変化はすごい早いです。その早さに置いていかれないように、今回は2020年9月現在の「ログイン画面」における「当たり前」を調査します。

まず、我らがくらしのマーケットのログインページはこちら。

くらしのマーケット

よくある感じですね、さて、置いていかれていないか調査に参ります。

世の中のログイン画面をみてみよう

Googleのログイン画面

Google

「メールアドレスまたは電話番号」と「パスワードを入力」する画面が別のページです。セキュリティ面の理由もありそうですが、ユーザーがログイン情報を覚えていないケースでは、この方が間違いがどちらにあるのか分かりやすそうですね。アプリでログインしようとしてもブラウザ版が起動するのも特徴です。「を入力」がパスワードの方にしか書いていないのは文字数が原因でしょうか。

Amazonのログイン画面

Amazon

「Eメールまたは携帯番号アカウントの番号」と「Amazonのパスワード」。 パスワードにわざわざ「Amazonの」と付いているのはなぜでしょう。 「アカウントの番号」なんてあるんですね、初めて知りました。

Facebookのログイン画面

Facebook

「電話番号またはメールアドレス」と「パスワード」の組み合わせです。シンプルで分かりやすく、今一番見かけるパターンな気がします。

Instagramのログイン画面

instagram

「電話番号、ユーザーネーム、メールアドレス」と「パスワード」の組み合わせです。サービスの特徴としてユーザーネームで覚えているユーザーが多そうですね。

LINEのログイン画面

LNE

「メールアドレス」と「パスワード」の組み合わせです。とってもシンプル!

Twitterのログイン画面

Twitter

「電話番号、メールアドレスまたはユーザー名」と「パスワード」の組み合わせです。Twitterもサービスの特徴としてユーザー名で覚えているユーザーが多そうですね。

Yahoo! JAPANのログイン画面

Yahoo! JAPAN

「ID/携帯電話番号/メールアドレス」と「確認コード」の組み合わせです。こちらも「ID」に当たる部分と「パスワード」に当たる部分の画面が別です。特徴としては、メールアドレスの場合でも、PCからのログインの場合でも、確認コードでログインする所です。パスワードを覚えなくていいのは便利ですね!

メルカリのログイン画面

メルカリ

まず最初に「アカウント連携」か、「メール・電話番号でログイン」かを選択します。 アカウント連携を選択するのユーザーが多いのでしょうか。「メール・電話番号でログイン」を選択後、「メールアドレスまたは電話番号」と「7文字以上の半角英数字」の組み合わせです。

UberEATSのログイン画面

UberEATS

「携帯電話番号」入力後、「OTPコードを入力してください」に4桁のコードを入力。SMSがうまくいかなかった時のために「パスワードを使用してログインする」が用意されています。こちらもブラウザ版が起動します。

TikTokのログイン画面

TikTok1

まず最初に「電話番号/メール/ユーザー名を使う」か、「アカウント連携」かを選択します。選べるアカウント連携の選択肢の数がすごいです!7つもあります! 「電話番号/メール/ユーザー名を使う」を選ぶと「ログイン」ページへ遷移し、タブで「電話」か「メールアドレス/ユーザー名」を選べます。

TikTok2

電話番号を入力すると4桁のコード入力画面に遷移しますが、こちらにも下の方に「パスワードでログイン」の導線がありました。クリックすると「パスワードを入力してください」の画面に行きます。SMSがうまくいかないケースが結構あるのでしょうか。

TikTok3

「メールアドレス/ユーザー名」のタブの中は「メールアドレスもしくはユーザー名」と「パスワード」の組み合わせでした。

TikTokは、ログインひとつに用意している選択肢の数が、圧倒的に多いですね!絶対ログインさせてあげるぞ!という気迫を感じます。

最後に

今回の調査で、「ログイン画面の当たり前」に若干遅れをとっていると感じました。「電話番号」が当たり前になりつつありますね。

「IDとパスワードは別のページ」という構成は、私の中でもっと多くのサービスの「当たり前」になっている印象でしたが、まだ「当たり前」にはなっていないのかなと感じた調査でした。

1,2年後に同じページの何がどう変化するのか、続編の調査も行いたいと思います!

AWS Summit Onlineにて、くらしのマーケットのインフラを紹介しています

昨日9/8より開催の、AWS Summit OnlineのAWS Startup Talksにて、「くらしのマーケット」のインフラ構成や、放映しているTVCMの負荷対策について、CTOの戸澤がご紹介しています。

aws.amazon.com (みんなのマーケット で、ページ内検索をお願いします)

くらしのマーケットは、2019年11月よりTVCMの放映をしています。 それまでスパイクのリクエストを処理した経験があまりなく、かつ、TVCMでどれほどの負荷が発生するかわからない状態でした。 予算を投じてTVCMを放映しても、ダウンしては意味がなく、更にユーザーにとってはネガティブな体験になります。 まずは負荷試験などできることから着手し、時間に限りもあったため、AWS含め対策を行い、放映を乗り切ることができました。 それらについて、また、業務で使用しているAWSサービスについても紹介しています。

ぜひご覧ください!