ことの発端
例えばこんな定義があったとします。
type Parameter = {
command: "start" | "stop";
duration: number;
};
こいつを初期化する際、色々と面倒な問題が出てきます。
何か問題でも?
返却値とオブジェクトの初期化 Parameter型
を生成する場合、下記のように <Parameter>
とか as Parameter
とか書くことが多いと思います。
function generateParameter() {
return <Parameter>{
command: "start",
duration: 100
};
}
function generateParameter2() {
return {
command: "start",
duration: 100
} as Parameter;
}
これはこれで、間違いではないです。
command
に "start"
か "stop"
を入れないとエラーになりますし、duration
も number型
を入れないとエラーになります。
間違いではないのですが、「間違いが見つかりにくい」という問題が生じます。
見つかりにくい問題
TypeScript での <Parameter>
や as Parameter
の取り扱いは
「型変換ではなく型チェックと型の上書き」
です。1
これがどういうことかというのは、下記のコードを見ると明らかです。
function generateParameter() {
return <Parameter>{
command: "start",
duration: 100,
debug: true
};
}
function generateParameter2() {
return {
command: "start",
duration: 100,
debug: true
} as Parameter;
}
Parameter型
の初期化に debug
という Parameter型
に定義されていないプロパティが登場していますが、これは型チェック的にはエラーになりません。つまり、command
と duration
が既定の型で初期化されていれば Parameter型
としては成立しているからです。(interface
の考え方と同じ)
今回の Parameter型
のように単純な型ならまだしも、メタプログラミングで交差型や合併型が複雑になってくると、間違いを見つけるのはかなり困難になります。
上記の例だと、デバッグモードなんて存在していないのに「あれれ?デバッグモードがある筈だから設定してみたけど動いてないな~」的な思い込み系のバグです。
または、Parameter型
がインターネット網を経由して対向サーバへデータを送信した際に、対向サーバでパラメータチェックを厳格にやっていてエラーレスポンスが返却され、「何がエラーなんだ!」と対向サーバ作っている人に文句を言ったらフルボッコされるケースなんかもあります。
ちゃんと書くと
で、ちゃんと書こうとするとこうなります。
function generateParameter() : Parameter {
return {
command: "start",
duration: 100
};
}
function generateParameter() {
const p: Parameter = {
command: "start",
duration: 100
};
return p;
}
上記の例では debug
みたいな Parameter型
に含まれないプロパティを指定するとエラーになります。
で、まぁ、TypeScriptの良さは高度な型推論にあると思うので、ちゃんと#1
のように関数定義で返却型を指定することは稀だったりします。(意図的に書く場合もありますが)
そうなると、 ちゃんと#2
みたいなパターンなると思うのですが、ちょっと手間だし、返却値を生成するためだけにローカル変数を定義するのもイマイチだったりします。
解決案
function as_<T>(o: T): T { return o; };
function foo() {
return as_<Parameter>({
command: "start",
duration: 100
});
}
TypeScriptは C++ のようにジェネリクス(template)の特殊化は行われないため、似たような関数がバカスカ生成されることはありません。
しかし、下記のような問題が考えられます。
- オブジェクトのコピーが発生
- 関数呼び出しのコストが発生
まず「オブジェクトのコピー」ですが、オブジェクトの実体がコピーされる訳ではなく、単なるポインタのコピー2なので問題ないでしょう。
次に「関数呼び出しのコスト」はオブジェクト初期化の定石であるコンストラクタ呼び出しと比較すると問題にならないんじゃないかと思います。
まとめ
単なる型チェックのために何もしない関数を作らなければならないので、TypeScriptの型チェックの機能として何かあれば良いのだが。。。
-
型アサーションとキャストの違い を参照 ↩
-
Javascriptではポインタという定義は無いのですが、概念として考慮しないと説明できないことが多くあります。(ディープコピーやシャローコピーなんかもそうですし、仮引数で受け取ったオブジェクトの操作と代入行為などもポインタを概念として取り入れないと説明が難しいです) ↩