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

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

緊急トラブルを解決するサービスをリリースしました!

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

最近寒くて布団から出るのが辛いPdMのハットリです。

今回は、緊急系サービスをリリースするにあたってどんな取り組みをしていたかを書いていきたいと思います。

緊急系サービスを一言で説明すると「24時間以内の作業を実現させるサービス」です。

詳しい説明は、PR担当の小田さんが書いた素晴らしいnoteがありますので、興味がある方はこちらをお読みください。

では緊急系チームについて書いていきます。

開発メンバー

  • カテゴリーマネージャー
  • PdM
  • デザイナー
  • エンジニア

開発手法

アジャイル開発

日々やっていたこと

デイリーMTG

毎日15時からチーム全員で集まりMTGをしていました。

進捗管理シートというもの作りそれを見ながら進捗確認を行っていました。

みんマではGitHubのissueベースで開発要件を整理しています。

3rdpartyプラグインのZenHubを使うことも考えたのですが、issue数や期限などが一覧で見にくいのとステータスの変更漏れなどがあるので、手間ですがスプレッドシートで管理するようになりました。

それを見ながら自分が担当しているissueの進捗共有と、困っていることがあればチームメンバーに相談して早期に解決するようにしていました。

毎日やると開発時間が減るのでは?と感じるかもしれませんが、15分〜30分程度の短時間で終わるのでそんなに影響ないのかなと思っています。


ウィークリーMTG

週次でチーム全員+CTOで集まりMTGをしていました。

デイリーMTGとは異なりissue単位での進捗共有ではなく、各自が今週やったことと来週やることを報告していました。


マンスリーKPT

月次でチーム全員で集まりKPTをしていました。

KPTとはプロダクトの進め方がどうだったのかを振り返り、「Keep」「Problem」「Try」の3項目を出し合い今後どう取り組んでいくかを決めます。

miroというツールを使い、各自KeepとProblemを出し合い全員でTryを考えました。

業務のKeepとProblemだけではなく、自然発生的にプライベートのKeepやProblemも出ていました。

ルールでプライベートのKeepやProblemも出してねって決めるのではなく、自然発生的に出てくるのがみんマっぽかったです。

月次で振り返ってみるとメンバーが同じようなKeepやProblemを持っていたことが分かりました。チーム全員が同じ認識を持っているのでTryに納得感があり来月からの行動に移しやすかったと思います。

業務のTryは改善策を真剣に考えて来月に生かす、プライベートのTryはちょっとふざけた改善策を出したりネタっぽくなっていたのもよかったです。

チーム発足後すぐにフルリモートになり、物理的に雑談する機会も減っていたのでKPT内で自然に雑談が生まれていたのもKPTのよかった点だなと思います。



うまくいったこと

リリース前にユーザーテストを行い最善の状態でリリースできたこと

プロダクトのリリース前に社員を対象にSUS評価を使ってユーザーテストを行っていました。

4回実施しリリース直前のユーザーテストでは、100点中82点を獲得しA評価のプロダクトになりました。

緊急系カテゴリの狙っていたポイント(会員登録から電話予約までスピーディーでスムーズ、わかりやすいUIなど)が評価されていたのが良かったです。

三者からの意見をもらうことで、プロダクトの方向性が間違ってないことが分かり自信にも繋がりました。

開発していると気がつけない部分や抜け漏れなど、他部署からのフィードバックはありがたかったです。


エンジニアとデザイナーがプロダクトの仕様理解度が高いこと

日々やっていたことの影響かもしれませんが、エンジニアとデザイナーのプロダクトの仕様理解度が高いなと感じました。

高い理由としては、エンジニアだからデザイナーだからという枠にとらわれず、自分が作っているプロダクトに興味を持っていたからだと思います。

その結果として実装時の要件漏れなどが少なかったです。

さらには「ユーザー的にはこうした方が使いやすくないですか?」など仕様の提案もしてくれました。

カテゴリマネージャーやPdMに仕様の最終的な決定権はあるのですが、少人数で考えると考慮漏れや抜け漏れが発生してしまいます。

エンジニアとデザイナーからの仕様の提案で考慮もれや抜け漏れが発生していたのに何度か気づくことができました。

どうせ仕様提案しても採用されないんでしょって投げやりになるのではなく、提案してくれたのは個人的にすごい嬉しかったです。



うまくいかなかったこと

issue内容やUIの更新ができていなかった

仕様やUIが変わった時に更新漏れがあり、issueの詳細やUIの内容が整理されておらず実装と解離がありました。

その結果、テストケース作成やテスト実施に影響があり想定以上にテストに時間がかかりました。

また、WebブラウザiOSアプリ、Androidアプリで実装に差異がある事もあり、都度仕様を確認する必要が発生しました。

アジャイル開発で開発を行っており、開発途中で仕様の変更・追加が多々あります。

その際にissue内容やUIの更新より先に実装を進めてしまったことに原因があると思っています。

開発スピードを求めた結果このようになってしまいました。

第二弾の緊急系カテゴリではその反省を生かし、仕様やUIが変わった際には開発に入る前にissueやUIの更新を行うようにしています。

リリースが1週間遅延してしまった

issue内容やUIの更新ができていなかったことが影響してリリースが1週間遅延してしまいました。

リリース後のKPTでチーム全員からリリースが遅延してしまって悔しいというProblemが出ました。

ほんとにあと少しのところで間に合わなかったです。

規模の大きい開発なので1週間は誤差でしょって考えもありますが、自分たちで決めたリリーススケジュールにあと少しのところで間に合わなかったのが悔しかったです。

今回の悔しさを晴らすために、第二弾の緊急系カテゴリはissueやUIの更新漏れをなくしスケジュール通りにリリースできるようにします。

最後に

緊急系サービスは生まれたばかりですが、今後飛躍的に成長するプロダクトだと思います!

一緒に開発したい方はぜひコーポレートサイトまでお気軽にご連絡ください!

ででんでんでん、デシジョンテーブル

ご無沙汰してます、QAエンジニアのざきです。
世の中の流れは早いもので、2020年も残り僅か。寒い地域は苦手なので宮崎から出られません。

はてさて

今回は数あるテスト設計技法の中からデシジョンテーブルについて書きます。

開発内容の規模が小さい場合、頭の中で仕様を巡らせテストケースを考える事は、そう難しくはありません。
しかしながら昨今のシステム開発では、開発の規模自体が大きく、1つの処理結果を得るためにも複数の条件や前提が存在する事がしばしばあります。
複数の条件が存在するだけでも頭が痛いのですが、その条件自体が複雑に絡み合い・入り乱れる事も多く、お手上げならぬ「思考停止」に陥ります。

QAとして、潜んでいる不具合を出来るだけ多く検出するために、正しい処理結果やユースケースを理解する事は避けて通れません。
「全数テストを行う事は不可能」というテスト原則もありますが、必要なテストケースをきちんと網羅できているのか、自分の中に不安が残る状態でテスト設計を行う事は避けたいです。

どうにか考え抜いたテストケースを並べて見た時に、「抜け・漏れがないか(網羅できているか)」「省略したテストは適切なのか(必要なものを省略していないか)」が瞬時に判断出来ない時、そんな時は深呼吸してデシジョンテーブルを作ってみましょう。

デシジョンテーブルとは

簡単に説明すると、入力データ・入力条件の組み合わせに対する処理・出力結果を表形式(テーブル構成)でまとめたものです。
日本工業規格『JIS X 0125 決定表』)で規格が定められていますが、実際の開発現場によって利用方法や作成方法は様々です。
この規格の中では「問題の記述において起こり得るすべての条件と、それに対して実行すべき動作とを組み合わせた表」と定義されています。

デシジョンテーブルの構成

デシジョンテーブルの構成は次の通りです。

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

ちょっとこれだけじゃ分かりにくいのでサンプルを1つ。
f:id:curama-tech:20201028124314p:plain 上の仕様に基づいてデシジョンテーブルを作成するとこうなります。

f:id:curama-tech:20201028124511p:plain
ドラッグストアの割引料金のデシジョンテーブル

デシジョンテーブルの解説

それでは、デシジョンテーブルの構成・要素について1つずつ解説します。

①条件の一覧(条件記述部 / condition stub)
 入力条件・入力データを列挙します。

f:id:curama-tech:20201028124821p:plain
①条件記述部

②結果の一覧(動作記述部 / action stub)
 条件に合わせて実行する動作や処理結果を列挙します。

f:id:curama-tech:20201028125035p:plain
②動作記述部

③条件の組合せ(条件指定部 / condition entry)
 条件の判定結果の組合せを指定します。判定結果は大半の場合、真か偽で表現します。
 表現方法として以下のものがありますが、どの組合せを使っても意味は同じです。
 [Y、N(Yes or No)][T、F(True or False)][1、0(真 or 偽)]

 組合せではなく、単独で真か偽を指定する場合は以下の表現方法を使用します。  [値、単語(この行に対応する条件が、記述された値や単語を満たすことを意味する)]
 [ -(この行に対応する条件が無関係であることを意味する)]

f:id:curama-tech:20201028125221p:plain
③条件指定部

④組合せに対応する結果(動作指定部 / action entry)
 条件の組合せに対応する動作結果を指定します。真か偽で表現する場合は条件指定部と同じ表現になります。
 [Y、N(Yes or No)][T、F(True or False)][1、0(真 or 偽)]

 その他の表現方法としては以下のものがあります。
 [X(eXecute):この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果となる]
 [空白、-:この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果の対象外となる]
 [値、単語:この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果が記述された値や単語を満たす]

f:id:curama-tech:20201028125310p:plain
④動作指定部

⑤規則
 入力条件、入力データの組合せと、それに対応する動作や処理結果を組合せたものです。
 デシジョンテーブルテストを行う際は、この各列(規則)をテストケースとして扱います。

f:id:curama-tech:20201028125435p:plain
⑤規則

サンプルの内容は複雑な条件組合せはありませんが、頭の中で考えるよりは理解しやすいのではないでしょうか。
デシジョンテーブルを利用すると、テストケースの内容を明確に示せるメリットも感じられると思います。
(箇条書きのテストケースは数が多いと読むのに疲れますよね ^^;)

デメリット

良いことばかりじゃないもので、デシジョンテーブルにもデメリットがあります。
これまでの経験・実践から感じているものは次の内容です。

  • 条件を1つ増やすだけで組合せ数が跳ね上がる(収拾がつかない)

  • 条件自体に抜け・漏れがあると全く意味が無い(ゆるゆるなテスト設計)

  • 実行順序を考慮できない(個人差が出ます、本当に)

テストケースとして利用する際には、テストフェーズや対象システム・機能項目と照らし合わせながら判断しましょう。
やたらめったらデシジョンテーブルにしてしまうと、逆に、とっても複雑なテストを作り出してしまいますのでご注意ください。

まとめ

今回は「デシジョンテーブル」についてお届けしました。ネーミングからして難しそうな技法を使えば、最良・最善のテストが出来る、なんて事はありません。あくまでも目的(より良いテスト)の為の手段なので、使い方(あなた)次第です。

小難しい用語や、自分にはしっくりこないルールなど、テスト設計技法も本当に様々あります。 個人的に、知らないよりは知った上で、必要なタイミングで必要なものを使えればそれが最高だな!と思ってます(゚∀゚)

最後まで読んでいただいた皆様、お付き合いありがとうございました!

Redisのコネクション数を10分の1にした話

はじめに

こんにちは、バックエンドエンジニアのtakayukiです。
私のあるリリースをきっかけとしてRedisのコネクション数が増加しました。 この記事にはRedisのコネクション数が増加した際の調査内容と修正の備忘録を書きます。

起きたこと

くらしのマーケットでは、非同期にタスクを実行する仕組みとしてCeleryというタスクキューを使用し、メッセージングキュー(メッセージブローカー)にはRedisを採用しています。

先日、このCeleryを利用した機能をリリースしたのですが、リリースの翌日にRedisのコネクション数が一定の基準値を超え、障害検知のアラートが鳴りました。
※くらしのマーケットではAWSのElastiCacheのRedisを使用しており最大接続数は65000ですがアラートは余裕をもって設定しています。

Redisのkeyとサイトへのリクエストを調査すると、私のリリースでCeleryのtaskを呼ぶ回数が増加し、それに伴ってコネクション数も増加していました。

調査

くらしのマーケットではバックエンドにPython製webフレームワークのDjangoを使用しており、そこからCeleryを経由してRedisにエンキューしています。 今回の私が作成したエンドポイントや他のエンドポイントでは、以下のようなCeleryのラッパークラスをインスタンス化してエンキューしていました。

from datetime import datetime
from celery import Celery

class CeleryClass(object):
    def __init__(self):
        self.app = Celery()
        self.app.config_from_object("config")

    def send_task(self, task_name: str, args: any, queue: str, eta: datetime=None):
        self.app.send_task(task_name, args=args, queue=queue, eta=eta)

# 使用するとき
Celery = CeleryClass()
Celery.send_task("SampleTask", args=[""], queue="user")

(一部名前など改変しています)

擬似コードなどを作って調べたところ、CeleryCelery()インスタンス化される時にコネクションを接続しているようです。しかし、このクラスを見ると共通クラスがインスタンス化される度にCelery()を呼んでいます。
どうやらこれが原因のようです。

解決策

解決策としては以下の2つが考えられました。

  1. Celeryにコネクションを明示的に閉じるコマンドが存在するので、コネクションが貼られる度に閉じる
  2. 生成するインスタンスを1つに制限するSingletonパターンを参考にしてCelery()インスタンスを使い回す

Celeryにはコネクションプール機能があり、明示的に閉じてしまうとその機能を生かせないため、2を選択し以下のように修正しました。

from datetime import datetime
from celery import Celery

class CeleryClass(object):
    __instance = None
    @staticmethod
    def get_instance():
        if CeleryClass.__instance is None:
            CeleryClass()
        return CeleryClass.__instance

    def __init__(self):
        if CeleryClass.__instance is not None:
            raise Exception("error")
        else:
            self.app = Celery()
            self.app.config_from_object("config")
            CeleryClass.__instance = self

    def send_task(self, task_name: str, args: any, queue: str, eta: datetime=None):
        self.app.send_task(task_name, args=args, queue=queue, eta=eta)

# 使用するとき
Celery = CeleryClass.get_instance()
Celery.send_task("SampleTask", args=[""], queue="user")

get_instanceインスタンスがあれば使い回し、なければ__instanceに保存するようにして、既存コードの修正を最小限にしました。

結果

結果として、コネクション数10分の1に少なくなりました。
一度下がった後にコネクション数が上がることなく推移したため、今回の修正で解決だと考えられます。

感想

今回の修正で無事Redisのコネクション数を下げることができました!
知識で知ってたことを実際に修正+体験して勉強になりました!

私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています。 一緒にくらしのマーケットを作っていきたい!という方はコーポレートサイト https://www.minma.jp/ までお気軽にご連絡ください!

TypeScript の特徴を他の言語と比較してみた

こんにちは。2020年4月に新卒で入社した片山です。

私は学生時代、主に C / C++ や Python を書いていました。TypeScript は くらしのマーケットのバックエンドで使われており、入社して初めて触った言語です。 これまで触った言語のとの違いや、特徴的な型推論について調べてみました。

この内容は 9/18 に宮崎で開催された Webナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~ で「TypeScript を初めて触った感想と型推論について」というタイトルで発表したものを記事として再編成・加筆したものになります。

言語の比較

学生時代触っていた C / C++、Python を TypeScript と比較しました。主観的な部分が含まれていますが、ご容赦ください。

実行速度の比較

Python

Python の標準の実装である CPyhon はインタプリタ言語であり、動的型付けを採用したことでコードを評価するたびに型のチェックが必要になることから、メモリアクセスの効率が悪いです。更に GIL の制約によりマルチスレッドのパフォーマンスも良くありません。しかし、PyPy のような JIT コンパイルを採用している処理系であれば CPython の3倍程度高速に実行することができるようです。

C / C++

最終的には機械語が出力されるため高速に実行できます。 学生時代作成したリバーシの思考ルーチンを Python から C++ に書き直した所約140倍速くなった経験があり、高速な言語といえばこれ、というイメージが強いです。

TypeScript

JavaScript にコンパイルして実行する必要があるので、JavaScript の実行速度になります。 Node.js では JIT Virtual Machine 型の V8 が使われており、高速に実行できます。

型についての比較

Python

整数や文字列、関数を含めたすべてのデータがオブジェクト(object)です。式の評価時に型をチェックする動的型付け言語です。

C / C++

char(8bit整数)、int(32bit整数 1)、float(32bit浮動小数点)と比較的低レベルな静的型付け言語です。型が違っていても暗黙的にキャストされるため、例えば以下のように意図しない動作をしてしまうことがあります。(重要じゃないコードは省略しています) また、高級言語ではあるものの、ポインタ や malloc関数 などの低レベルな処理を書くことができる反面、メモリ安全性を考慮する必要があり注意することが多いです。

char c = 'A'
int a = c;  // 65

double f = 3.14;
int b = f; // 3

int i = 100000;
short s = i;  // -31072

TypeScript

静的型付け、動的型付けのどちらも使うことができます。また、プリミティブ型は以下の種類しかなく、C言語と比べるとシンプルです。整数と小数を区別されておらず、どちらも number型 です 2。また、後述の型推論を使うことで、最低限のアノテーションで型安全なプログラムを記述できます。

  • number
  • bigint
  • string
  • boolean
  • null
  • undefined
  • symbol

パフォーマンスの比較

個人的には実行速度が気になりましたので、以下のような再帰関数で35番目のフィボナッチ数( = 9,227,465 )を求めるプログラムで実行時間を計測してみました。 シンプルなプログラムですが、計算量が文字通り指数的に増大します。複雑な計算がないので計算のコストよりも関数呼び出しのコストが大きく出るかもしれません。

# Python
def f(n):
    if n <= 1:
        return n
    else:
        return f(n - 2) + f(n - 1)

y = f(35)
言語 (実行条件) 実行時間
C 言語 ( GCC -O3 ) 0.027 s
C 言語 ( GCC -O2 ) 0.037 s
C 言語 ( GCC ) 0.074 s
JavaScript (Node.js) 0.097 s
Python ( PyPy ) 0.35 s
Python ( CPython ) 3.5 s

実行環境
OS: Ubuntu 18.04
CPU: AMD Ryzen 5 2600
RAM: 8GB 2667Hz 2枚

実行環境や使用したソフトウェアのバージョンによって結果が変わるので参考程度の実測値です。 JavaScript は GCC でコンパイルした C言語には敵いませんが、PyPy で実行した Python よりも約30倍速かったです。

型推論について

TypeScript で特徴的だと感じたのが型推論の機能です。C言語では必ず型を宣言する必要があり、型が違っていても暗黙的にキャストされるため代入できる場合があります 3。TypeScript では型を指定せずとも 3 を number 型と推論されます。それによって一貫性を守るように強制されるので型によるバグが減ります。

// C言語
char s[] = "ABC";
int i = 3;
i = s;  // -647713340 !?
// TypeScript
const s: string = "ABC";
let i = 3
i = s  // ERROR !!

次に、どのような記述なら正しく推論されるのかを調べてみました。プリミティブ型や、プリミティブ型で初期化したオブジェクトは問題なく型推推論できました。 ただ、宣言時に初期化をしない場合では any型 になってしまいます。初期化無しで変数を宣言するにはアノテーションが必要です。

// TypeScript
const i = 15;        // number
const I = 9007199254740991n; // bigint
const s = “hello”;   // string
const b = true;      // boolean
const n = null;      // null
const u = undefined; // undefined
const sym = Symbol("foo");  // symbol

// { name: string; sales: { jpy: number; usd: number; }[] }
const complex = {
  name: "taro",
  sales: [
      {jpy: 1200, usd: 12},
      {jpy: 810, usd: 8.1},
  ],
};

// 宣言をしたあとに代入
let x;
x = 3;  // any

let y: number;
y = 3;  // number

関数(の返り値)でも型推論は機能しますが、number型同士でのみ許可される * 演算子を戻り値にしたような場合だけです。関数の場合はアノテーションした方が良さそうです。

// TypeScript

// 返り値が推論できる
function mul(a, b){
    return a * b;
}
const y1 = mul(7, 5);  // number

// 返り値が推論できない
function add(a, b){
    return a + b;
}
const y2 = add(7, 5);  // any

既存の JavaScript のライブラリで型安全の恩恵を受ける

アンビエント宣言を記述することで、JavaScript で書かれたライブラリを TypeScript から呼び出す際に型安全の恩恵を受けることができるようになります。例えば以下はコミュニティが管理している Node.js の console.log のアンビエント宣言の抜粋です。(関係のない部分は省略してます)

interface Console {
  Console: NodeJS.ConsoleConstructor;
  log(message?: any, ...optionalParams: any[]): void;
}
<200b>
declare var console: Console;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v8/base.d.ts

まとめ

TypeScript は適切に書くことで、効率よく型安全なプログラムを作ることとができ、パフォーマンスも優れた言語だと感じました。しかし、基本である JavaScript の仕様を完全に把握できておらず、業務で混乱することがありましたので、まだまだ勉強が必要そうです!!


  1. int型のbit数はC言語の規格として定義されていないため、CPUによって64bitや、マイコンでは8bit、16bitが使われることがあります。
  2. 64bitの浮動小数点が使われていますが、整数を格納すると53bit精度になるようです。より大きな整数はbigint型で宣言します。
  3. char*(ポインタ)からintへ暗黙的にキャストされたため、アドレスが代入されています。また、実行ごとにこの値は変化します。

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 を使うのがおすすめ

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