TypeScriptでStateパターンな電卓アプリを作る

新たにTypeScriptの勉強を始めたので、知識定着の為にTypeScriptを用いて電卓を作成しました。
特にライブラリやフレームワークは利用していません。
TypeScriptの基本知識は一通り目を通した方向けです。

# 参考
- [公式サイト](https://www.typescriptlang.org/)
- [速習TypeScript ※Kindle](https://www.amazon.co.jp/dp/B0733113NK)
- [TypeScript Deep Dive](https://basarat.gitbooks.io/typescript/)

# 環境

- Windows 10
- Node.js 10.15.1
- TypeScript 3.5.3
- webpack 4.37.0

※コンパイル後のコードはes2015です。

[最新版TypeScript+webpack 4の環境構築まとめ](https://ics.media/entry/16329/)

# Stateパターンな電卓
## 完成図
![電卓.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/5f5b9d90-d7a3-a982-e7a0-27051ab13a97.gif)

実物はこちらです。キーボード入力にも対応しています。
https://beebow6.github.io/typescript-calculator/dist/

全てのソースはこちらです。
https://github.com/BeeBow6/typescript-calculator

## 状態遷移表
TypeScriptの基本的なところをなるべく一通り体験すべく、Javaで電卓アプリを作成している下記サイトを参考に作成しました。
[サンプルアプリでおぼえる実践的Android入門](https://codezine.jp/article/detail/5957)※要事前登録

今回の電卓の状態遷移表は下記のとおりです。

||A:左辺入力前<br>BeforeLeftSideState|B:数値入力<br>NumberState|C:演算子入力<br>OperatorState|D:右辺入力後<br>AfterRightState|E:結果表示<br>ResultState|F:エラー<br>ErrorState|
|:--|:--|:--|:--|:--|:--|:--|
数値<br>0-9<br>.|数値表示<br><font color="MediumBlue">⇒Bへ</font>|数値追加表示|数値表示<br><font color="MediumBlue">⇒Bへ</font>|乗算演算子登録<br>数値表示<br><font color="MediumBlue">⇒Bへ</font>|演算履歴削除<br>数値表示<br><font color="MediumBlue">⇒Bへ</font>|-|
円周率<br>π|円周率表示<br><br><font color="MediumBlue">⇒Bへ</font>|円周率表示|円周率表示<br><font color="MediumBlue">⇒Bへ</font>|乗算演算子登録<br>円周率表示<br><font color="MediumBlue">⇒Bへ</font>|演算履歴削除<br>円周率表示<br><font color="MediumBlue">⇒Bへ</font>|-|
演算子<br>＋－×÷|0表示<br>数値確定<br>演算子登録<br><font color="MediumBlue">⇒Cへ</font>|数値確定<br>演算実行<br>演算子登録<br><font color="MediumBlue">⇒Cへ</font>|演算子差し替え|演算子登録<br><font color="MediumBlue">⇒Cへ</font>|演算履歴削除<br>数値確定<br>演算子登録<br><font color="MediumBlue">⇒Cへ</font>|-|
左括弧<br>(|左括弧登録|数値確定<br>乗算演算子登録<br>左括弧登録<br><font color="MediumBlue">⇒Aへ</font>|左括弧登録<br><font color="MediumBlue">⇒Aへ</font>|乗算演算子登録<br>左括弧登録<br><font color="MediumBlue">⇒Aへ</font>|すべて削除<br>左括弧登録<br><font color="MediumBlue">⇒Aへ</font>|-|
右括弧<br>)|-|数値確定<br>右括弧登録<br>演算実行<br><font color="MediumBlue">⇒Dへ</font>|-|右括弧登録|-|-|
符号反転<br>+/-|符号反転<br><font color="MediumBlue">⇒Bへ</font>|符号反転|表示削除<br>符号反転<br><font color="MediumBlue">⇒Bへ</font>|乗算演算子登録<br>符号反転<br><font color="MediumBlue">⇒Bへ</font>|演算履歴削除<br>符号反転<br><font color="MediumBlue">⇒Bへ</font>|-|
結果表示<br>=|-|数値確定<br>演算実行<br><font color="MediumBlue">⇒Eへ</font>|演算子破棄<br>演算実行<br><font color="MediumBlue">⇒Eへ</font>|演算実行<br><font color="MediumBlue">⇒Eへ</font>|-|-|
一つ削除<br>←|-|一つ削除|-|-|演算履歴削除<br>一つ削除<br>⇒数値なし<font color="MediumBlue">⇒Aへ</font><br>⇒数値あり<font color="MediumBlue">⇒Bへ</font>|-|
削除<br>C|-|表示削除<br><font color="MediumBlue">⇒Aへ</font>|表示削除|-|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|-|
全て削除<br>AC|すべて削除|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|すべて削除<br><font color="MediumBlue">⇒Aへ</font>|

[State パターン | TECHSCORE](https://www.techscore.com/tech/DesignPattern/State.html/)

# 定数を列挙型（enum）で定義
入力される数値および小数点と各種演算子、ボタンタイプを[列挙型（enum）](https://www.typescriptlang.org/docs/handbook/enums.html)で定義します。

```ts
/**
 * ボタンからの入力値およびボタンタイプを列挙型で定義
 */

// 演算子
enum OPERATOR {
  ADD = '+',
  SUBTRACT = '-',
  MULTIPLY = '*',
  DIVIDE = '/',
  LEFT_PAREN = '(',
  RIGHT_PAREN = ')'
}

// 数値
enum NUMBER {
  ZERO = '0',
  ONE = '1',
  TWO = '2',
  // ...
  EIGHT = '8',
  NINE = '9',
  POINT = '.'
}

// ボタンの種類
enum BUTTON_TYPE {
  NUMBER = '10',
  PI = '11',
  OPERATOR = '20',
  LEFT_PAREN = '21',
  RIGHT_PAREN = '22',
  INVERSION = '23',
  EQUAL = '80',
  CLEAR = '90',
  ALL_CLEAR = '91',
  BACKSPACE = '92'
}
```

[/src/setting.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/setting.ts)

数値で宣言した場合、定義外の数値もすり抜けるようですね。
![typescript_enum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/88e8190e-1e64-1910-d7cd-4bfb87d2d269.png)

# VIEWコンポーネント
## 抽象クラス
[ReactのComponent](https://ja.reactjs.org/docs/react-component.html)みたいなVIEWコンポーネントの基底クラスを[抽象クラス](https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes)で定義し、ボタンやディスプレイ等の各コンポーネントはこれを継承するようにします。

![継承.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/841a3010-8371-6e78-981f-2a3ea3ae2868.png)

## 基底クラスの定義
DOM要素を生成・保持します。
生成した要素は読み取り専用で参照可能です。
抽象メソッド`getTemplate()`では、各派生クラスでHTMLソースを定義させるようにしています。
このソースがプレースフォルダを含む場合、`constructor`で受け取ったマップ情報をもとに置換します。

```ts
/**
 * コンポーネントの基底クラス
 */

interface ReplaceMap {
  [key: string]: string | number;
}

export default abstract class Component<T> {
  // このクラス内でのみ参照可。派生クラスでも参照不可。
  private _element: T;

  constructor(map: ReplaceMap = {}) {
    const div: HTMLElement = document.createElement('div');
    div.innerHTML = this.replaceTemplate(this.getTemplate(), map);
    this._element = div.firstElementChild as unknown as T;
  }

  // 派生クラスで要実装
  protected abstract getTemplate(): string;
  // このクラス内でのみ参照可。派生クラスでも参照不可。
  private replaceTemplate(temp: string, map: ReplaceMap = {}) {
    return temp.replace(/\{\{(.+?)\}\}/g, (_, key) => map[key] as string || '');
  }
  // どこからも参照可能。読み取り専用。
  get element(): T {
    return this._element;
  }
}
```

[/src/components/component.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/components/component.ts)

### ジェネリック型(Generics)

DOM要素を保持するメンバ`_element`は[ジェネリック型](https://www.typescriptlang.org/docs/handbook/generics.html)で定義しています。
一律で`HTMLElement`としてしまうと、`<input>`の`value`プロパティなど、一部の要素しか実装していないプロパティは存在しないプロパティとみなされてしまいます。
そのため、派生クラス側で[HTMLInputElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement)や[HTMLButtonElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLButtonElement)などの具体的な型を指定させます。
`div.firstElementChild`で返される要素の型は`Element`か`null`なので、Type Assertions (キャスト)で指定の型に変換する必要があるのですが、直接`div.firstElementChild as T`とすると、間違っていないかい？と駄目だしされました。
意図したものであることを伝えるには、`div.firstElementChild as unknown as T`と、間に`unknown`を入れる必要があるそうです。

## 派生クラスで実装

### ボタン
ボタンは下記リスト情報をもとに生成されます。

```ts
interface ButtonParams {
  text: string,
  type: BUTTON_TYPE,
  order: number,
  keyCodeList: string[],
  value?: NUMBER | OPERATOR,
  size?: number
}

const BUTTONS: ButtonParams[] = [
  {
    text: NUMBER.ZERO,
    value: NUMBER.ZERO,
    type: BUTTON_TYPE.NUMBER,
    order: 21,
    keyCodeList: ['48', '96']
  },
  {
    text: NUMBER.ONE,
    value: NUMBER.ONE,
    type: BUTTON_TYPE.NUMBER,
    order: 17,
    keyCodeList: ['49', '97']
  },
  // ...
};
```

Componentクラスを継承したボタンクラスでは、オーバーライドしたメソッド`getTemplate()`でHTMLソースを定義しています。
ボタン情報リストの型定義をした`ButtonParams`を拡張した`ButtonProps`を、コンストラクタの引数のインターフェースとしています。

```ts
/**
 * ボタンコンポーネント
 */

interface ButtonHandler {
  (type: BUTTON_TYPE, value?: NUMBER | OPERATOR): void;
}

interface ButtonProps extends ButtonParams {
  onClick: ButtonHandler;
}

class Button extends Component<HTMLButtonElement> {

  private type: BUTTON_TYPE;
  private value: NUMBER | OPERATOR;
  private keyCodeList: string[];
  private onClick: ButtonHandler;

  constructor({ text, order, size, ...props }: ButtonProps) {
    super({ text, order });

    this.element.style.gridColumnEnd = size ? 'span ' + size : '';

    if (props.type === BUTTON_TYPE.NUMBER) {
      this.element.classList.add('btn-num');
    }

    this.type = props.type;
    this.value = props.value;
    this.keyCodeList = props.keyCodeList;
    this.onClick = props.onClick;

    this.element.addEventListener('click', this.handleClick.bind(this));
    // ...
  }

  // Override
  getTemplate() {
    return `<button type="button" class="btn" style="order:{{order}}">
      {{text}}
    </button>`;
  }

  private handleClick() {
    this.onClick(this.type, this.value);
  }

  // ...
}
```

[/src/components/button.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/components/button.ts)

### パネル
ボタン等の設置先となるパネルコンポーネントは、パネル上にComponentを追加する為の`add()`メソッドを持ちます。
このときもジェネリック型は明示する必要があるのですが、[HTMLElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement)とすることで、複数種類の要素を受付けられるようにしています。

```ts
/**
 * 派生クラス:パネルコンポーネント
 */
import Component from './component';

class Panel extends Component<HTMLFormElement> {
  // Override
  getTemplate() {
    return '<form class="panel"></form>';
  }

  add(parts: Component<HTMLElement>) {
    this.element.appendChild(parts.element);
  }

  // ...
}
```

[/src/components/panel.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/components/panel.ts)

### ディスプレイクラス
入力中の数値を表示する`MainDisplay`クラスと、演算のプロセスを表示する`ProcessDisplay`クラスの2種があります。
どちらも`Button`や`Panel`同様、`Component`クラスの派生クラスですが、入力値を制御・保持する為のロジックを持ちます。

#### MainDisplay
内部で入力値、小数点の有無、正負を管理しています。
小数点ボタンが重複してクリックされても、2回目以降の入力は受付けません。
また、12文字以上の数値は受付けません。（符号除く）
入力値は文字列で管理されていますが、パブリックメソッド`getNumber()`で数値として取得できます。

```ts
const LENGTH_MAX: number = 12;

class MainDisplay extends Component<HTMLInputElement>{

  private data: string = '';
  private isDecimal: boolean = false;
  private isNegative: boolean = false;

  constructor() {
    super();
    this.clear();
  }

  get isEmpty(): boolean {
    return this.data === '';
  }

  getTemplate() {
  }
  // 末尾に数字を追加
  addNumber(value: NUMBER) {
    if (this.data.length >= LENGTH_MAX) return;

    // ...
    this.data += value;
    this.displayNumber();
  }
  // 既存データを削除して数字表示
  setNumber(value: NUMBER | number) {
    this.reset();

    // ...
    this.displayNumber();
    return;
  }
  // 符号反転
  invertSign() {
    this.isNegative = !this.isNegative;
    this.displayNumber();
  }

  getNumber() {
    return Number.parseFloat(this.element.value) || 0;
  }
  // 末尾の一文字削除
  removeLastNumber() {
    this.data = this.data.slice(0, -1);
    // ...
    this.displayNumber();
  }
  // 削除
  clear() {
  }
  // エラーメッセージ表示
  setError(message: string) {
  }
  // 表示
  private displayNumber() {
    const value = (this.isNegative ? MINUS : '') + this.data;
    this.element.value = value || NUMBER.ZERO;
  }
  // リセット
  private reset() {
    this.data = '';
    this.isDecimal = this.isNegative = false;
  }
  // エラーモード
  private toggleErrorMode(flg = false) {
    this.element.classList.toggle('is-error', flg);
  }
}
```

[/src/components/mainDisplay.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/components/mainDisplay.ts)

#### ProcessDisplay
入力された数値と演算子を順番に配列で保持しています。
左括弧の入力数を管理しており、余分な右括弧が入力されないようにしています。
配列を出力する際に右括弧が不足している場合は追加します。
負数は見やすいよう括弧で括ります。

```ts
class ProcessDisplay extends Component<HTMLInputElement> {

  private stack: (number | OPERATOR)[] = [];
  private currentOperator: OPERATOR | null = null;
  private countParen: number = 0;

  constructor() {
    super();
    this.clear();
  }

  getTemplate() {
  }
  // 括弧利用中か
  get isParenMode(): boolean {
    return this.countParen > 0;
  }
  // 数値登録
  setNumber(number: number) {
    this.setStack(number);
  }
  // 演算子登録
  setOperator(operator: OPERATOR = null) {
    this.currentOperator = operator;
    this.display();
  }
  // 左括弧登録
  setLeftParen() {
    this.countParen++;
    // ...
  }
  // 右括弧登録
  setRightParen() {
    if (!this.countParen) return;
    this.countParen--;
    // ...
  }
  // 履歴リスト取得
  getStack(): (number | OPERATOR)[] {
    const rightParens = Array(this.countParen).fill(OPERATOR.RIGHT_PAREN);
    return [...this.stack, ...rightParens];
  }
  // 結果表示
  setResult() {
    this.stack = this.getStack();
    this.display();
  }
  // 履歴削除
  clear() {
  }
  // 履歴追加
  private setStack(value: number | OPERATOR) {
    // ...
    this.display();
  }
  // 表示
  private display() {
    this.element.value = [...this.stack, this.currentOperator].join('');
  }
}
```

[/src/components/processDisplay.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/components/processDisplay.ts)

# 四則演算
演算を実際に行う部分はクラスではなく関数で定義しています。
JavaScriptは仕様上、小数点の演算に誤差が発生してしまうのですが、一旦整数に直して演算することでそれを回避しています。
また、この電卓は括弧にも対応している為、[逆ポーランド記法](https://ja.wikipedia.org/wiki/%E9%80%86%E3%83%9D%E3%83%BC%E3%83%A9%E3%83%B3%E3%83%89%E8%A8%98%E6%B3%95)を用いて演算しています。
引数の型チェックの手間が省けている事意外は、ほぼ普通のJavaScriptの関数なので割愛いたします。
（別記事で書くかも。。。）

[/src/arithmetic.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/arithmetic.ts)
[/src/calculation.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/calculation.ts)

[JavaScriptで、できるかぎり小数演算の誤差を少なくする方法](https://am-yu.net/2016/10/29/javascript-decimal-calc/)
[日曜プログラミングで電卓を作ってみる](https://knowledge.sakura.ad.jp/220/)

# 電卓クラス
電卓の基本機能を備えたクラス`App`を定義します。
各コンポーネントのインスタンスを生成して`Panel`上に設置します。
ボタンクリックイベント発生時に、後述するStateクラスに転送します。
入力値の管理は2つのディスプレイクラスに[委譲](https://ja.wikipedia.org/wiki/%E5%A7%94%E8%AD%B2)しているため、必要に応じてこれらのメソッドを呼び出すだけです。

```ts
// 型チェック
const isNUMBER = (test: any): test is NUMBER => {
  return Object.values(NUMBER).includes(test);
};

class App {

  private panel: Panel;
  private process: ProcessDisplay;
  private display: MainDisplay;
  private state: State;

  constructor(rootElement: HTMLElement) {
    // コンポーネントの設置
    this.panel = new Panel();
    this.process = new ProcessDisplay();
    this.display = new MainDisplay();

    this.panel.add(this.process);
    this.panel.add(this.display);

    this.handleClick = this.handleClick.bind(this);
    
    BUTTONS.forEach(props => {
      this.panel.add(
        new Button({
          ...props,
          onClick: this.handleClick
        })
      );
    });

    rootElement.appendChild(this.panel.element);

    this.switchState(InitialState.instance);
  }

  /**
   * ボタンクリック時に、ボタンのタイプに応じてStateの各メソッド呼び出し
   * @param {BUTTON_TYPE} type
   * @param {String} value 
   */
  private handleClick(type: BUTTON_TYPE, value: NUMBER | OPERATOR) {
    switch (type) {
      case BUTTON_TYPE.NUMBER:
        if (isNUMBER(value)) {
          this.state.inputNumber(this, value);
          break;
        }
      case BUTTON_TYPE.PI:
        this.state.inputPi(this, Math.PI);
        break;
    // ...
  }

  // 状態切替
  switchState(nextState: State) {
    this.state = nextState;
  }
  // 数値表示
  displayNumber(value: number | NUMBER) {
    this.display.setNumber(value);
  }
  // 数値追加表示
  addDisplayNumber(value: NUMBER) {
    this.display.addNumber(value);
  }
  // 演算子登録
  setOperator(operator?: OPERATOR) {
    this.process.setOperator(operator);
  }
  // 数値確定
  determineNumber() {
    /**
     * MainDisplayクラスが保持する入力値を取得して、
     * ProcessDisplayクラスの入力履歴に登録する
     */ 
    const num: number = this.display.getNumber();
    this.process.setNumber(num);
  }
  // 括弧利用中確認
  checkParenMode() {
  }
  // 左括弧登録
  setLeftParen() {
  }
  // 右括弧登録
  setRightParen() {
  }
  // 符号反転
  invertSign() {
  }
  // 演算実行
  executeCalculation() {
    /**
     * ProcessDisplayクラスが保持する入力履歴を取得して、四則演算関数へ渡す
     * 演算結果を取得して、MainDisplayクラスで表示させる
     */ 
    const result: number = Calc(this.process.getStack());
    this.display.setNumber(result);
  }
  // 表示1文字クリア
  backSpaceDisplay(): boolean {
  }
  // 表示クリア
  clearDisplay() {
  }
  // 履歴クリア
  clearHistory() {
  }
  // 全てクリア
  clearAll() {
  }
  // エラー！！！
  setError(e: Error) {
  }
  // ...
}
```

[/src/app.ts | GitHub](https://github.com/BeeBow6/typescript-calculator/blob/master/src/app.ts)

### 型ガード（Type Guard）
`App`クラスにて、クリック時にボタンから呼ばれるコールバック関数`handleClick()`の第二引数`value`は、列挙型の`NUMBER`もしくは`OPERATOR`が渡されます。
一方、転送先の`State`クラスの各メソッドは、どちらかしか受付けません。
`NUMBER`であるかを確認する関数`isNUMBER()`で、事前に型ガードを施しています。

[TypeScriptでみかける"is"というキーワードについて](https://tech-1natsu.hatenablog.com/entry/2019/02/02/200829)

# 状態（State）クラス
TypeScriptにあってJavaScriptにない、[Interface](https://www.typescriptlang.org/docs/handbook/interfaces.html)を利用して上記の状態遷移を実現します。

![Stateパターン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362737/e55ef642-119a-0169-1744-655f1259f8e0.png)


## StateのInterfaceの定義
それぞれのボタンごとに、クリック時の振る舞いを定義するメソッドを用意します。
`App`クラスの`handleClick()`メソッドから呼び出されます。

```ts
interface State {
  // 数値ボタンクリック
  inputNumber(app: App, value: NUMBER | number): void;
  // 円周率ボタンクリック
  inputPi(app: App, value: number): void;
  // 演算子ボタンクリック
  inputOperator(app: App, value: OPERATOR): void;
  // 左括弧ボタンクリック
  inputLeftParen(app: App): void;
  // 右括弧ボタンクリック
  inputRightParen(app: App): void;
  // 符号反転ボタンクリック
  inputInversion(app: App): void;
  // イコールボタンクリック
  inputEqual(app: App): void;
  // 一つ削除（←）ボタンクリック
  inputBack(app: App): void;
  // クリアボタンクリック
  inputClear(app: App): void;
  // オールクリアボタンクリック
  inputAllClear(app: App): void;
}
```

## StateInterfaceの実装
上記インターフェースを実装する数値入力状態`NumberState`は下記のとおりです。
渡された`App`クラスのインスタンスを必要に応じて操作するだけで、内部でデータは持ちません。
インスタンスを複数作成する必要が無い為、[シングルトン](https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)で作成しています。

```ts
class NumberState implements State {
  // シングルトン
  private static _instance: NumberState;
  // コンストラクタがプライベートなので、外部からnewできない
  private constructor() { }
  // 静的プロパティからのみ取得可能
  static get instance(): NumberState {
    if (!this._instance) {
      this._instance = new NumberState();
    }
    return this._instance;
  }

  inputNumber(app: App, value: NUMBER) {
    app.addDisplayNumber(value);
  }
  inputPi(app: App, value: number) {
    app.displayNumber(value);
  }
  inputOperator(app: App, value: OPERATOR) {
    try {
      app.determineNumber();
      app.executeCalculation();
      app.setOperator(value);
      app.switchState(OperatorState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputLeftParen(app: App) {
    app.determineNumber();
    app.setOperator(OPERATOR.MULTIPLY);
    app.setLeftParen();
    app.switchState(BeforeLeftSideState.instance);
  }
  inputRightParen(app: App) {
    if (!app.checkParenMode()) return;

    try {
      app.determineNumber();
      app.setRightParen();
      app.executeCalculation();
      app.switchState(AfterRightSideState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputInversion(app: App) {
    app.invertSign();
  }
  inputBack(app: App) {
    if (app.backSpaceDisplay()) {
      app.switchState(BeforeLeftSideState.instance);
    }
  }
  inputEqual(app: App) {
    try {
      app.determineNumber();
      app.executeCalculation();
      app.toggleAnswerMode(true);
      app.switchState(ResultState.instance);
    } catch (e) {
      app.setError(e);
    }
  }
  inputClear(app: App) {
    app.clearDisplay();
    app.switchState(BeforeLeftSideState.instance);
  }
  inputAllClear(app: App) {
    app.clearAll();
    app.switchState(BeforeLeftSideState.instance);
  }
}
```

未入力もしくは左括弧入力直後の状態`BeforeLeftSideState`は下記の様になります。
右括弧などの入力不可のイベントは、空の関数でオーバーライドしています。

```ts
class BeforeLeftSideState implements State {

  private static _instance: BeforeLeftSideState;
  private constructor() { }
  static get instance(): BeforeLeftSideState {
    if (!this._instance) {
      this._instance = new BeforeLeftSideState();
    }
    return this._instance;
  }

  inputNumber(app: App, value: NUMBER | number) {
    app.displayNumber(value);
    app.switchState(NumberState.instance);
  }
  inputPi(app: App, value: number) {
    this.inputNumber(app, value);
  }
  inputOperator(app: App, value: OPERATOR) {
    app.displayNumber(NUMBER.ZERO);
    app.determineNumber();
    app.setOperator(value);
    app.switchState(OperatorState.instance);
  }
  inputLeftParen(app: App) {
    app.setLeftParen();
  }
  inputInversion(app: App) {
    app.invertSign();
    app.switchState(NumberState.instance);
  }
  inputRightParen() { /* 対応しない */ }
  inputEqual() { /* 対応しない */ }
  inputBack() { /* 対応しない */ }
  inputClear() { /* 対応しない */ }
  inputAllClear(app: App) {
    app.clearAll();
  }
}
```

`State`側で必要に応じて`app.switchState()`を呼び出して他の`State`への切り替えを行い、`App`側では`State`の変更を意識する必要はありません。
便利ですね。他所でも使えそう。

[TypeScript 2ではシングルトン(Singleton)パターンが短く書ける](https://qiita.com/tonkotsuboy_com/items/225d08e915a57777c9dc)

# おわりに
以前はVBAをよく書いており、その際は必ず型を指定していました。
その為JavaScriptを始めたばかりの頃は、型指定が出来ないことを気持ち悪いな～と感じていました。
いつの間にか慣れてしまってはいたものの、型はそれとなく意識しながら書いていたので、Typescriptにさほど戸惑うことはありませんでした。
やはり、型を指定できるほうが便利だし楽しいな～と感じました。