「静的型付き言語を使おうが、文字列型は変わらずバグの温床である」というような話を見かけたので、文字列の自由奔放さに抗って型に嵌め殺すために今できることについて考えてみた。
例えばこういうミス
/** @param url {string} URL文字列を指定してください。 */
function printURL(url: string): void {
console.log(url);
}
printURL('焼肉食べたい'); // コンパイラ「OK!」
静的型付けの恩恵をまったく生かせていないコード。
人類は愚かなので、コンパイラがOKだと言えばそれを信じるし、typoだってする。
あまりにもイケてない。どうすればいいのか。
取りうる値が決まっているとき
独自定義したいくつかのイベントに名前をつけて、文字列で判別する。というのはよくあるパターンだと思う。こういったケースには割と簡単にstaticなやり方を持ち込むことができる。
定数を定義して使う
const EVENT_START: string = 'start';
const EVENT_PAUSE: string = 'pause';
function emitGameEvent(eventName: string): void {}
emitEvent(EVENT_START);
脳みそが停止していると一番最初に書くコードだけれど、ごちゃごちゃしやすいのであんまりやりたくない。
しかも型はstring
のままなので、結局関係のない値を突っ込めてしまう。
これは型安全性のためにやることではない。気休めにしかならない。
String Literal Type を使う
type GameEvent = 'start' | 'pause' | 'end';
function emitGameEvent(event: GameEvent): void {}
emitGameEvent('start'); // OK!
emitGameEvent('stop'); // コンパイルエラー
TypeScriptらしい素晴らしい機能。
簡単に取りうる値を列挙できる上に、妙な値を指定としたときはきちんとコンパイルエラーを吐いてくれる。
非常にスマートでよい。こういうのが欲しかったんだ。
他所で使わない場合は、引数の型として直接指定することもできる。
function emitGameEvent(event: 'start' | 'pause' | 'end'): void {}
String Enums を使う
enum GameEvent {
START = 'start',
PAUSE = 'pause',
END = 'end'
}
function emitGameEvent(event: GameEvent): void {}
emitGameEvent(GameEvent.START);
TypeScript 2.4から導入された String Enums 。
上記のString Literalと大体同じようなものなのだけれども、こっちのほうがリファクタリングはしやすいと思う。
取りうる値に幅があるとき
URLやらファイルパスやら、決め打ちにはできないけれど、一定の法則を持つ値を扱う場合の話。
コンパイル時に拾うのは難しいし、だいたい問題になるのはこういったケースだと思う。
正規表現でチェックする
function printURL(url: string): void {
assert(urlPattern.test(url), `urlとして不適切な書式 "${url}"`);
console.log(url);
}
型自体はstring
のままなのでコンパイル時には拾えていない。バグは発生する。
ただ実行時に早い段階でエラー原因を捕捉できるので、最低限このくらいはやっておきたいという話。
ラッパークラスを活用する
import { URL } from 'url';
function printURL(url: URL): void {
console.log(url);
}
const target: URL = new URL('https://example.org/foo#bar');
printURL(target);
文字列型のままだからいけないのだ。
URLやらファイルパスなんかには、大抵ラッパークラスが用意されているのだから、
できるだけ関数の引数などではラッパークラスの型で指定しておき、極力生の文字列のまま取り廻さないようにすればいい。
そうすればエラー原因となりうる箇所の範囲は最小化できるはずだ。(無くなるとは言っていない)
独自のパターンをもつ文字列に対して、ラッパーを定義して使うのも有効だと思う。
class UserID {
readonry value: string;
private static pattern: RegExp = /^[A-Z]\\d{4}$/;
public constructor(value: string) {
assert(UserID.pattern.test(id), `不正な形式のUserID "${id}"`);
this.value = value;
}
}
ただ若干面倒くさいし、冗長になってしまうかもしれない。
濫造するのもよくない気はする。
Decoratorでチェック
(サンプルコードを書こうと思ったけど挫折)
Javaのフレームワークなんかを使っていると、Annotationで値チェックなんてことをよくやる。JSでもDecoratorで値の正当性をチェックすることもやることがあるかなぁ、とだけ考えた。
実装するのはかなり面倒だし、これもやっていることは実行時のアサーションに過ぎないのでそもそも静的型付的なアプローチでもない。
でもDecoratorってなんかかっこいいよね。
まとめ
プレーンなJavaScriptよりは当然できることは多いのだけれども、
コンパイラがなんでもかんでもに目を配ってくれるわけではない。
まあコンパイラの庇護をできるだけ広い範囲で受けられるようなコードを書こう、という感じ。
それが無理なところは、せめて実行時チェックするしかないよね。
typeを正規表現で定義できるようになったりしないですかね?(脳みそからっぽ)