LoginSignup
268
181

More than 3 years have passed since last update.

TypeScript: 「電話番号型」という意味を持たせた特殊なstring型の作り方

Last updated at Posted at 2020-04-24

この投稿では、値としては「文字列」なんだけど、単なる文字列ではなく「電話番号型」という意味を持たせた文字列型を定義し、それ使用する方法を紹介します。

↑上のような疑問に対する答えです。

実現方針

方針としては、以下のテクニックの組み合わせです。

  • 公称型で、「電話番号型」を定義する
  • ユーザ定義タイプガードで、文字列型を電話番号型としてコンパイラに認識してもらう

公称型とその実装方法についての基本は下記投稿をご覧ください。

ユーザ定義タイプガードについては下記ページが参考になります。

具体的な実現方法

公称型で「電話番号型」を定義する

ここでは、公称型のテクニックを使って、電話番号型を定義する方法を説明します。

先に、これから定義する電話番号型の特徴を決めておきます:

  • string型は電話番号型に代入できない。つまり、2つの型は区別される。
  • 電話番号型はstring型に代入できる。つまり、電話番号型は文字列として扱うことができる。

それでは、実際にこの特徴を満たす型を定義していきます。

まず、電話番号型としてPhoneNumberという型エイリアスを定義します。

type PhoneNumber = string

これはstring型にPhoneNumberという別名をつけただけです。なので、このままだと、stringPhoneNumberをコンパイラは区別しません。相互に代入可能です。

let a: string = '123'
let b: PhoneNumber = '456'
a = b // OK
b = a // OK

このままではダメなので、PhoneNumber型を公称型にします。公称型にするテクニックの基礎は下記投稿で説明されているので詳細は省きますが、

ざっくり言うと、シンボルを持ったインターフェイスとstring型の交差型(intersection type)を定義して公称型を実現します:

declare const phoneNominality: unique symbol
type PhoneNumber = string & { [phoneNominality]: never }

これで、コンパイラがstring型とPhoneNumber型をしっかり区別してくれるようになります:

let a: string = '123'
let b: PhoneNumber = '456' as PhoneNumber

a = b // OK
b = a // Error: Type 'string' is not assignable to type 'PhoneNumber'.

ちなみに、PhoneNumberの定義に使用したdeclare const phoneNominalitytype PhoneNumber =...などの型レベルプログラミングコードは、コンパイル後のJavaScriptコードからは綺麗サッパリ消え去り、JavaScriptの振る舞いを変更するような処理は残らないので安心してください。下記コードを見ての通り、PhoneNumber型はJavaScript実行時はごくごく普通の文字列として扱われます:

// コンパイル前コード
declare const personNominality: unique symbol
type PhoneNumber = string & { [personNominality]: never }
let a: string = '123'
let b: PhoneNumber = '456' as PhoneNumber

// コンパイル後コード
"use strict";
let a = '123';
let b = '456';

以上で、文字列型と区別できる「電話番号型」の定義の完成です。

最後に、電話を掛ける関数callPhoneを書いて、実際に電話番号型だけを受け付けるか、コンパイルを試してみます。

const callPhone = (phoneNumber: PhoneNumber): void => {
    console.log('%oに電話をかけてます……', phoneNumber)
}

const justString: string = '電話番号じゃないかも文字列'
const phoneNumber: PhoneNumber = '090-9999-0000' as PhoneNumber

callPhone(justString) // Error: Argument of type 'string' 
                      //   is not assignable to parameter of 
                      //   type 'PhoneNumber'.
callPhone(phoneNumber) // OK

callPhoneに渡されたただの文字列justStringはコンパイルエラーになるのに対し、電話番号型文字列のphoneNumberはエラーなく渡せているのが分かります。

ユーザ定義タイプガードで、文字列型を電話番号型としてコンパイラに認識してもらう

続いて、ユーザ定義タイプガードを使って、文字列型の値をバリデーションした上で、その値を電話番号型として扱えるようにするコードを書いていきます。

最初のステップとして、文字列が電話番号かどうかチェックする関数を実装します:

const isPhoneNumber = (value: string): boolean => {
    return /^[0-9]+-[0-9]+-[0-9]+$/.test(value)
}

このisPhoneNumber関数は、与えられた文字列が「数字-数字-数字」のフォーマットかを正規表現で判定し、フォーマットに合っていればtrueを、そうでなければfalseを返す関数です。(ちなみに、電話番号の正規表現としては厳密な実装ではないと思いますので、業務等で使う場合は、要件と仕様をよく検討してしかるべき正規表現を組んでください)

次に、上のisPhoneNumberをユーザ定義タイプガード関数にしたいので、戻り値のbooleanをちょっと書き換えます:

const isPhoneNumber = (value: string): value is PhoneNumber => {
    return /^[0-9]+-[0-9]+-[0-9]+$/.test(value)
}

この関数をifなどで使い、チェックされた文字列は、その後の処理では型レベルで電話番号型と認識されるようになります:

const wouldBePhoneNumber: string = '090-9999-0000'

// この時点では`string`型なのでエラーになる↓
callPhone(wouldBePhoneNumber) // Error: Argument of type 'string' 
                              //  is not assignable to parameter 
                              //  of type 'PhoneNumber'.

if (isPhoneNumber(wouldBePhoneNumber)) {
    // このスコープでは`PhoneNumber`型として解釈されるのでコンパイルエラーにならない↓
    callPhone(wouldBePhoneNumber) // OK
} else {
    console.log('%oは電話番号ではありません', wouldBePhoneNumber)
}

以上、TypeScriptで「電話番号型」という意味を持たせた特殊なstring型の作り方でした。

まとめ

最後に完成形の全体コードを載せておきます:

declare const phoneNominality: unique symbol
type PhoneNumber = string & { [phoneNominality]: never }

const callPhone = (phoneNumber: PhoneNumber): void => {
    console.log('%oに電話をかけてます……', phoneNumber)
}

const isPhoneNumber = (value: string): value is PhoneNumber => {
    return /^[0-9]+-[0-9]+-[0-9]+$/.test(value)
}

const wouldBePhoneNumber: string = '090-9999-0000'

callPhone(wouldBePhoneNumber) // Error: Argument of type 'string' is not assignable to parameter of type 'PhoneNumber'.

if (isPhoneNumber(wouldBePhoneNumber)) {
    callPhone(wouldBePhoneNumber) // OK
} else {
    console.log('%oは電話番号ではありません', wouldBePhoneNumber)
}

このコードのTypeScript Playgroundはこちら


最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:Twitter@suin

268
181
3

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
268
181