L08084のブログ

技術記事の執筆は、祈りに近い

【Angular】シャイニングブルートウキョウでは、プログラムがあなたを条件分岐する!!

f:id:l08084:20181224205343p:plain

はじめに

Vue.jsの公式ドキュメントに、質問するとユーザーの代わりにYES or NOの意思決定を行ってくれるWebAPI(YesNo.WTF)が紹介されていたのをきっかけに、AngularでYesNo.WTFを呼び出して結果を表示するアプリを作ってみました。

タイトルはお馴染みのロシア的倒置法をもじったもので、シャイニングブルーというのは2020東京オリンピックのボランティア名の候補の一つだそうですよ。

引用元の記事

算出プロパティとウォッチャ — Vue.js

今回の記事では、上記のVue.jsのサンプルコードをAngularに書き換えてみます。

試しに触ってみる

完成版をGitHub Pagesに上げているので、実際に触って遊ぶこともできます!

YES or NO形式の質問を入力して500ミリ秒待つと質問に対する適当な回答(YES or NO)と愉快なGIF画像が表示されるぞ!

完成版のデモのリンク(https://l08084.github.io/ng-yes-or-no/)

全体のコードに興味がある人はGitHubに掲載されている下記コードを参照のこと

github.com

実装

バージョン情報

JavaScriptのフレームワークとしてAngularを、CSSのフレームワークとしてAngular Materialを使いました。

  • angular@7.0.7
  • angular/material@7.2.0
  • angular/cli@7.0.7
  • rxjs@6.3.3

質問フォーム作成

まず(YesNo.WTF)に投げかける質問を入力する質問フォームを作成します。

質問フォームのコンポーネントクラス

Angularのモデル駆動型フォーム(Reactive駆動型)を使用していて、全角か半角のを文末に入力しないといけないバリデーションもかけています。

質問フォームのテンプレート

// 入力フォームの値が変更された場合、500ミリ秒間隔で値を取得する
this.questionForm.get('question').valueChanges.pipe(debounceTime(500))
    .subscribe((searchText) => {

Vueのサンプルコードでは、lodash_.debounce(func, [wait=0], [options={}])を使用してyesno.wtf/apiへのリクエスト回数を制限していましたが、本コードではAngularらしく(?)RxJSdebounceTimeを利用してAPIへのアクセスを500ミリ秒間隔に制限しています。

あと全然知らなかったんですが、RxJS 6系だとpipe()メソッドを使わないとオペレーター(この例だとdebounceTime)使えないんですね......

回答を表示するコンポーネントを作成する

続いて、APIの回答結果(YES or NOとGIF画像)を表示するコンポーネントを作成していきます。

回答を表示するコンポーネントクラス

回答を表示するコンポーネントのテンプレート

作っててGIF画像って<img>タグで表示するんだ〜って思いました。まあそりゃそうでしょって感じですが

動作確認

実際に質問を入力してみましょう。ちゃんと動いていますね

f:id:l08084:20181224194508p:plain
実際に質問している様子

あとがき

サンプルコードの写経以上、クソアプリ未満といった感じでした。明日からはもうちょいがんばろう。ね、ハム太郎

【Angular】 ボタンの連打を防止するディレクティブを作る(連続クリック・二重送信の禁止)

はじめに

アプリケーションを作る上で避けられない、ボタンの連続クリック防止機能を今回は作成します。この機能がないと同じメールが2通飛んだりデータベースにレコードが重複してINSERTされてしまうかも......

バージョン情報

JavaScriptのフレームワークとしてAngularを使用しています

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1

実装

今回のテーマである連続クリックの防止ですが、ディレクティブを使用して実現します。
AngularのディレクティブにはNgForとNgIfに代表される構造ディレクティブとテンプレート要素の属性を変更する属性ディレクティブがあり、今回作成するのは後者の属性ディレクティブになります。

ボタンの連打を防止するAngularのディレクティブ

このディレクティブの機能としては、HTTP通信(非同期処理)が発生するボタンでの使用を想定していて、押下したときにHTTPの通信が終わるまで、ボタンを連打されないように非活性化するといったものになっています。

今回作成したディレクティブの処理の流れ

  1. ElementRefを通してボタンのHtmlElementを取得する
  2. @HostListener('click')でボタンがクリックされたイベントを検知し、ボタンを非活性化(disabled)する
  3. HTTP通信が終了したことを@Inputデコレーターを通して検知したら、ボタンの非活性化を解除する

HTMLからディレクティブを呼びだす

引数を受け取るタイプのディレクティブなので、このようにHTMLテンプレート上で引数を渡してあげる必要があります

今回作成したディレクティブの呼び出し例

終わりに

スピナー(くるくる回るやつね)使えばよくねえか???と思った人もいるかもしれませんが、デザイン的にスピナーを使えない事情があったのでボタンを非活性にする方法でやってみました。
あと0.5秒とか1.0秒とか追加で非活性の時間のばしてるけど(エクセルを信頼せずに電卓で計算をやり直す側の人間なので)、完全に無駄な処理だったかもしれない。せっかくHTTP通信中のフラグをInputで渡しているわけだしそのフラグのON・OFFだけでボタンの非活性・活性を制御しても良かった気がしています

【Angular】複数項目にまたがるカスタムバリデーションを作る(パスワード・確認用パスワードなど)

f:id:l08084:20181125171015p:plain
パスワード・確認用パスワード入力フォームの例

はじめに

アカウント登録画面を作っていて出現頻度が著しく高い項目といえば............そう!パスワードの入力フォームですね。というわけで今回は、パスワードと確認用パスワードが一致しているか確認するバリデーションを実装していきます。

そもそもAngularでカスタムバリデーションをどう作っていいかわからねーよという人は下記の記事を!

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション) - L08084のブログ

Angular Materialのインストール方法と使い方については、下記の記事を参照してください。

Angular Material の Tableを使う - L08084のブログ

Angular Materialでログインフォームを作る - L08084のブログ

バリデーション内容

パスワードについてのバリデーションを設定します。チェックする項目は下記とします。

  • パスワード欄と確認用パスワード欄で入力された内容が一致する

バージョン情報

JavaScriptのフレームワークとしてはAngularを、CSSのフレームワークとしてはAngular Materialを使用しています。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

パスワード入力フォーム作成

前回の記事で作成した入力フォームにコードを足していく感じで実装していきます。
コードが多くなってきて前回からの更新部分がわかりづらいですが、<!-- add this! -->とコメントされている行が追加された部分になります。

f:id:l08084:20181124201537p:plain
前回の記事で作成した入力フォーム

まずテンプレートに、パスワード入力フォームと確認用パスワードの入力フォーム、そしてパスワードが表示しないときに表示するエラーメッセージを表示します。

app.component.html

<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <!-- 氏名の入力フォーム -->
        <mat-form-field class="input-field">
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須項目です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- add this! -->
        <!-- パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'password' : 'text'" matInput
            placeholder="パスワード" id="password" name="password" [formControl]="nameRegisterForm.controls.password" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.password.errors?.required">パスワードは必須項目です</mat-error>
        </mat-form-field>
        <!-- 確認用パスワードの入力フォーム -->
        <mat-form-field class="input-field">
          <input [type]="hide ? 'confirmPassword' : 'text'" matInput
            placeholder="確認用パスワード" id="confirmPassword" name="confirmPassword" [formControl]="nameRegisterForm.controls.confirmPassword" required>
          <mat-icon matSuffix (click)="hide = !hide">{{hide ? 'visibility' : 'visibility_off'}}</mat-icon>
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.required">確認用パスワードは必須項目です</mat-error>
          <!-- add this! -->
          <!-- パスワードが一致しないときに表示するエラー -->
          <mat-error *ngIf="nameRegisterForm.controls.confirmPassword.errors?.notMatchPassword">パスワードが一致しません</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

コンポーネントクラスにもパスワード入力フォームに関するコードを追加します。(テンプレートの時と同じく追加部分には// add this!とコメントしています)

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;
  // add this!
  public passwordControl: FormControl;
  public confirmPasswordControl: FormControl;
  public hide = true;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
    // add this!
    this.passwordControl = this.nameRegisterForm.get('password') as FormControl;
    this.confirmPasswordControl = this.nameRegisterForm.get('confirmPassword') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]],
      // add this!
      password: ['', [Validators.required]],
      confirmPassword: ['', [Validators.required]]
    }, {
      validator: CustomValidator.matchPassword
    });
  }
}

今回追加するのは複数項目に対するバリデーションなので、他のバリデーションと違ってvalidator: CustomValidator.matchPasswordという風にフォーム全体にバリデーションを定義しています。

最後にパスワード入力フォームに入力された内容と確認用パスワードに入力された内容が一致していることを確認するカスタムバリデーションmatchPasswordメソッドを追加します。

custom-validator.ts

import { ValidationErrors, FormControl, AbstractControl } from '@angular/forms';

export class CustomValidator {

  // 姓と名の間にスペースが入っているか確認する
  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }

  // add this!
  // パスワードと確認用パスワードが一致するかチェック
  static matchPassword(ac: AbstractControl) {
    const password = ac.get('password').value;
    const passwordConfirm = ac.get('confirmPassword').value;
    if (password !== passwordConfirm) {
      ac.get('confirmPassword').setErrors({ notMatchPassword: true });
    }
  }
}

単項目に対するバリデーションであるhaveBlankには引数としてFormControlを渡していますが、複数項目に対するバリデーションであるmatchPasswordにはAbstractControlを渡しています。

動作確認

書いたコードを動かしてみます

f:id:l08084:20181128091521p:plain
初期表示

一致しないパスワードを入力するとエラーが表示されます。

f:id:l08084:20181128091844p:plain
パスワード不一致のエラーメッセージが表示されている

一致するパスワードを入力するとエラーが表示されなくなるので、バリデーションが正しく機能していることがわかります。

f:id:l08084:20181128092002p:plain

【JavaScript】文字列の間に空白(スペース)が入力されているか確認する(氏名などのバリデーション)

はじめに

アカウント登録画面でよく見る姓と名の入力欄について、フォームが二つに別れているパターンと、フォームが一つになっていてスペースで姓と名を区切るパターンの二つがあると思います。

今回は後者を採用した場合に必要になるバリデーションについて書きます(なぜなら今作ってるシステムがそうなので)。

とはいえ、他のWebサービスを見ると基本的に姓と名でフォームを分けてる場合がほとんどですね。なんでフォームを一つにしちゃったんだろう。。。デザインの問題?画面が縦に長すぎるとまずいとか

f:id:l08084:20181118142046p:plain
姓と名が別れている入力フォームの例

バリデーション内容

氏名についてのバリデーションを設定します。チェックする項目は下記とします。

  • 氏名に少なくとも1つのスペース(半角もしくは全角)が入力されていること
  • ただし、スペースが文頭または文末に入力されている場合は、未入力の扱いとする
  • 複数スペースが入力された場合は、最初のスペースを姓名の区切りとして利用する

バージョンなどの情報

JavaScriptのフレームワークとしてはAngularを使用していますが、他フレームワークや生JavaScriptでもバリデーションのロジック部分は変わらないと思います。

  • Angular@7.0.6
  • typescript@3.1.6
  • webpack@4.19.1
  • Angular Material@7.0.4

実装

CSSフレームワークとして、Angular Material を使用しています。Angular Material自体のインストール方法や使い方は下記の記事を参照してください。

Angular Material の Tableを使う - L08084のブログ

Angular Materialでログインフォームを作る - L08084のブログ

Angularのバリデーション機能は、テンプレート(HTML)に検証ルールを記述するテンプレート駆動型と、コンポーネント側に検証ルールを記述するモデル駆動型に分かれていますが、本記事ではモデル駆動型の方を使用しています。

まず姓と名の間にスペースが入力されているかを判定するカスタムバリデーションを作成します。

  • custom-validator.ts
import { ValidationErrors, FormControl } from '@angular/forms';

export class CustomValidator {

  static haveBlank(control: FormControl): ValidationErrors | null {
    const value = (control.value || '') + '';
    // 両端のスペースを取り除く
    const name = value.trim();
    // 半角スペース
    const NAME_COLUMN_SPRIT_VALUE = ' ';
    // 全角スペース
    const NAME_COLUMN_SPRIT_VALUE_W = ' ';
    let isError = false;
    if (name.indexOf(NAME_COLUMN_SPRIT_VALUE) < 0
    && name.indexOf(NAME_COLUMN_SPRIT_VALUE_W) < 0) {
        // 半角スペースも全角スペースも含まれていない場合
        isError = true;
    }
    // 姓と名の間にスペースが入力されていない場合は、バリデーションエラーを返す
    return isError ? { haveBlank: true } : null;
  }
}

実装内容としては、文字列の両端のスペースを取り除いた後に、半角または全角のスペースが入力されているかどうかをindexOf()メソッドを使用してチェックしています。(indexOf()メソッドの代わりにincludes()メソッドを使ってもOKです)

続いて、作成したカスタムバリデーションをフォームの方に設定してあげると、実装が完了します。

  • app.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';
import { CustomValidator } from './custom-validator';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {

  public nameRegisterForm: FormGroup;
  public nameControl: FormControl;

  constructor(private builder: FormBuilder) {
    this.createForm();
  }

  public ngOnInit(): void {
    this.nameControl = this.nameRegisterForm.get('email') as FormControl;
  }

  public onSubmit() {
    console.log();
  }

  /**
   * フォームグループの初期化を実行する
   *
   * @private
   * @memberof AppComponent
   */
  private createForm() {
    // 氏名欄のバリデーションを設定している
    this.nameRegisterForm = this.builder.group({
      name: ['', [Validators.required, CustomValidator.haveBlank]]
    });
  }
}
  • app.component.html
<div class="container">
  <mat-card class="login-card">
    <mat-card-header>
      <mat-card-title class="login-title">アカウント登録</mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <form [formGroup]="nameRegisterForm" (ngSubmit)="onSubmit()" class="login-form">
        <mat-form-field class="input-field">
          <!-- 氏名の入力フォーム -->
          <input matInput placeholder="氏名(姓と名の間にスペースを入力してください)"
            id="name" name="name" [formControl]="nameRegisterForm.controls.name" required>
          <!-- 必須入力のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.required">氏名は必須入力です</mat-error>
          <!-- スペースがない場合のエラーメッセージ -->
          <mat-error *ngIf="nameRegisterForm.controls.name.errors?.haveBlank">姓と名の間にはスペースを入力してください</mat-error>
        </mat-form-field>
        <!-- 登録ボタン -->
        <button type="submit" class="register-button"
          mat-raised-button color="primary" [disabled]="nameRegisterForm.invalid">登録</button>
      </form>
    </mat-card-content>
  </mat-card>
</div>

スペースが入力されなかった場合のエラーメッセージについては、テンプレート(HTML)の方に記載しています。

動作確認

実際に動かしてみます。

f:id:l08084:20181124201537p:plain
初期表示

姓と名の間にスペースが入力されていない氏名を入力すると、ちゃんとエラーメッセージを表示してくれます。

f:id:l08084:20181124201731p:plain

姓と名の間にちゃんとスペースを入力してあげると、エラーメッセージが消えて登録ボタンもアクティブになりました。成功ですね f:id:l08084:20181124201929p:plain

おわりに

Angularのカスタムバリデーションですが、他にもディレクティブ機能を使う方法などがあり、また今回の例だとそもそもカスタムバリデーションを作らずに正規表現で解決する方法もありました。実際のシステム開発だと姓と名それぞれの文字数制限などのバリデーションが追加されるであろうことを考えると、ディレクティブを使ったり正規表現を使ったほうがキレイなコードが書けたのでは.....みたいな気持ちがあります。でも次回への課題ということでこの記事自体は終了します。

アカウント登録メールの実装をしています

f:id:l08084:20181110163634p:plain
アカウント登録メールの例

はじめに

Webサービスで会員登録の情報を入力した後に、メールアドレス確認のメールが飛んでくるサービスよくありますよね。
あれの実装をいま仕事でしています。よくある機能だと思うので、実装方針を備忘録として記録しました。

作成しているシステムについて

アカウント登録機能のあるWebアプリケーションで、採用している言語・ミドルウェアは下記の一覧を参照

  • Java
  • Angular
  • Ionic
  • PostgreSQL
  • AWS

メールの具体的な仕様

今回実装しているアカウント登録メールの大まかな仕様

  1. ユーザーが仮登録(メールアドレスなどを入力)を完了する
  2. 本登録画面へのURLリンクを含んだメールをユーザーに送信する
  3. URLリンクをクリックしたユーザーを認証して問題なければ、本登録画面に遷移させる

この記事では、URLリンクをクリックしたアカウントの認証(というか特定)方法に絞って解説します。
HTMLメールのレイアウトなどについては、こちらの記事をご参照ください。

アカウント登録のためのURLリンクを作成する

アカウント登録メールでは、アカウント本登録画面に遷移するURLだけではく、クリックしたユーザーを特定する情報が必要になります。

本システムでは、メールを送信する前の画面で入力させる情報がメールアドレスしかなかったため、URLのパラメーターにメールアドレスを含めることでアカウントを特定することにしました。

メールに載せるURLのイメージとしてはこんな感じです。
https://[システムのドメイン]/[アカウント本登録画面のパス]/[暗号化したメールアドレス]

メールアドレス暗号化の手順

URLに生のメールアドレスをそのまま載せるのはセキュリティ的にアレだよね...ってなったので暗号化することにしました。

  1. サーバサイド(Java)でAWS Key Management Service (KMS)を利用してメールアドレスを暗号化する
  2. 暗号化したテキストをBase64エンコードする
  3. 2.のテキストをURLエンコードする

3.URLエンコードを最初忘れていたせいで、暗号化したテキストにURLに使用できない文字が混じって認証に失敗したりしました。

ユーザーがURLリンクをクリックした時の動作としては、URLから本登録画面に遷移した後、WebAPIを呼び出して暗号化したテキストをデコードして元のメールアドレスに戻し、DBを参照してアカウントを特定するという流れにしています。

言いたいこと

完全に霊感で作ったので、その実装ヤバくね?みたいなやつがあったらコソッと教えてください。まだ間に合います

【Vue.js】Nuxt.jsのひな形を作成する

Nuxt.jsに入門したいので、ひな形の作成からはじめました。

バージョン情報

  • npm 5.6.0
  • yarn 1.5.1

手順

ターミナルを開いて、下記のコマンド実行する。

$ npm i -g @vue/cli @vue/cli-init

Vue CLIが正しくインストールされていることを確認する。

$ vue -V
3.0.3

vue initコマンドでVue.jsのひな形を作成する。
vue initコマンド後に色々と聞かれるが、特にこだわりがなければ全部EnterでOK。

$ vue init nuxt-community/starter-template <project-name>

上記のコマンドでは、nuxt-community/starter-templateというスターターテンプレートを使用して、Vue.jsのプロジェクトを作成している。

$ cd <project-name>
$ yarn # npm iでも可、パッケージのインストールを行なっている
$ yarn dev # npm run dev でも可

上記のコマンド実行後に、ブラウザでhttp://localhost:3000を開くと下記のNuxt.jsのデフォルト画面が表示される。

f:id:l08084:20181021171754p:plain
Nuxt.jsのデフォルトの画面

参考サイト

インストール - Nuxt.js

【JavaScript】return なしでもPromiseはメソッドチェーンできる

はじめに

then関数に新しいPromiseオブジェクトを返す機能があるので、then内でPromiseをreturnする処理を書かなくても、Promiseはメソッドチェーンが可能だということを最近知ったのでメモ。

Promiseチェーンの色々な例

はじめにで述べたことを理解するために、色々なタイプのPromiseをメソッドチェーンで繋ぎます

returnなし

then()内でreturnしない場合でも、thenが新しいPromiseを返してくれるのでメソッドチェーンが可能です

// new Promise(resolve => resolve()) と同じ
const promise = Promise.resolve();

// 実行結果
//    A
//    B
//    C
promise.then(console.log('A'))
       .then(console.log('B'))
       .then(console.log('C'));

returnあり

チェーンの次の処理に前の処理の結果を渡したい場合は、then()内に渡したい値をreturnする処理を書く必要があります。
returnした値はthenの機能でPromiseオブジェクトに変換されるので、数値や文字列だけでなく、オブジェクトでもPromiseでもどの値を返しても、Promiseはメソッドチェーンが可能です

数値を伝搬するPromiseチェーン

数値をreturnで次の処理に渡しています。returnした数値はthenの機能でPromiseオブジェクトに変換して渡されます

// new Promise(resolve => resolve()) と同じ
const promise = Promise.resolve();

// 実行結果
// 6
// arrow functionを使っているのでreturnは省略されているが、
// 実際には数値がreturnされている
promise.then(() => 1)
       .then(value => value + 2)
       .then(value => value + 3)
       .then(value => console.log(value));
Promiseを伝搬するPromiseチェーン

PromiseをPromiseチェーンを使って次の処理に渡すこともできます。
次の例では、戻り値がPromiseのサードパーティ製ライブラリのメソッドをreturnしています。

// 戻り値がPromiseのメソッド
this.keychainTouchId.isAvailable()
    .then((res: any) => {
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId.has(BioAuthService.KEY_A);
    }).then((res: any) => {
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId
            .verify(BioAuthService.KEYCHAIN_KEY, `ロックを解除してください`);
    }).then((res: any) => {
        this.password = res;
        // 戻り値がPromiseのメソッド
        return this.storage.get(BioAuthService.KEY_B);
    }).then((res: any) => {
        this.userId = res;
        const params = {
            loginId: this.userId,
            password: this.password,
            deviceToken: null
        };
        this.action.login(params);
    }).catch((error: any) => {
        // catchは一つでよい
        console.error(error);
    });

結論

何も考えずにthenでつないどけば、Promiseはメソッドチェーンが可能

参考サイト

JavaScript Promiseの本

Promiseを使う - JavaScript | MDN