TypeScriptを使っているなかで、気になった挙動に遭遇したので備忘録として残しておきます。
例えば、標準のErrorクラスを拡張して、InvalidError
というカスタムエラーのクラスを作るとします。
class InvalidError extends Error { constructor(message: string) { super(message) this.name = 'InvalidError' } }
先程定義した InvalidError
型の変数を引数に取る、関数を作ります。
const someFunc = (err: InvalidError) => { if (err instanceof InvalidError) { console.log('invalid errorだよ') } else { console.log('invalid errorじゃないよ') } }
呼び出してみます。
someFunc(new InvalidError('hoge')) // [LOG]: "invalid errorだよ"
もちろん想定通り動きました。
引数に、標準のErrorインスタンスを与えてみます。
someFunc(new Error('piyo')) // [LOG]: "invalid errorじゃないよ"
標準のErrorインスタンスは、引数で指定されている InvalidError
の親クラスなので、一見すると引数の型が不一致であるように見えますが、IDE上でも実行時もエラーの表示がありません。
一方 instanceof
の判定は false となっていました。
「挙動がおかしい?!」と、一瞬思いましたが、タイトルにあるとおり、2つの型システムの違いからこのような挙動の差が生まれています。
引数での型指定と、instanceofでの型チェックはチェックされるタイミング、チェックされ方が異なるのです。
引数での型指定
トランスパイル時にチェックされます。したがって、TypeScriptの型システムの考え方に従って、チェックされます。
トランスパイルされたコードを確認してみると、次のようになっています。
const someFunc = (err) => { if (err instanceof InvalidError) { console.log('invalid errorだよ'); } else { console.log('invalid errorじゃないよ'); } };
型指定はなくなってますね。
このことからもTypeScriptの型システムのみでチェックされていることがわかります。
(トランスパイルされたJavascriptのコードに型情報がなければ、Javascriptでチェックしようがないですよね)
TypeScriptの型の考え方ですが、「構造的部分型」という考え方で構築されています。
詳細を知りたい方は、例えば次のページを参照されると良いと思います。
typescript-jp.gitbook.io typescriptbook.jp
今回作成したカスタムクラスと、標準のErrorクラスは構造が同じになっています。
したがって、TypeScript的には型は同じということでエラーや警告は出ません。
エラーや警告を出すためには、次のように、標準のErrorクラスにはないフィールドを定義するようにするといいと思います。
(idのリテラル型の指定や値の指定が煩雑なので、もっといいやり方を知っている方いれば、ぜひ教えてください)
class InvalidError2 extends Error { id: 'InvalidError2' = 'InvalidError2' constructor(message: string) { super(message) this.name = 'InvalidError' } } const someFunc2 = (err: InvalidError2) => { if (err instanceof InvalidError2) { console.log('invalid errorだよ') } else { console.log('invalid errorじゃないよ') } } someFunc2(new Error('piyo')) // IDEなどで次のようなエラーが表示される // Argument of type 'Error' is not assignable to parameter of type 'InvalidError2'. // Property 'id' is missing in type 'Error' but required in type 'InvalidError2'.
instanceofでの型チェック
トランスパイルされたあとのJavascriptのコードを確認してみます。
const someFunc = (err) => { if (err instanceof InvalidError) { console.log('invalid errorだよ'); } else { console.log('invalid errorじゃないよ'); } };
instanceofはJavascriptの関数なので、Javascriptのコードにそのまま残っています!!
つまり、Javascriptのルールに従って型のチェックがされます!
Javascriptはプロトタイプベースの型システムが構築されており、prototypeを辿っていって、継承関係があるかどうかチェックしていきます。 詳しくは、次のMDNのドキュメントを読まれるといいと思います。
今回、InvalidError
はprototypeが InvalidError
になっているので、instanceofはtrueとなります。
しかし、標準のErrorのprototypeチェーンには Object
と null
しかいないので、instanceofはfalseとなります。
おわりに
TypeScriptで型のチェックと言っても次の2つのチェックタイミング、型システムが混在することを意識しなければなりません。
- トランスパイル時
- TypeScript:構造的部分型によるチェック
- 実行時
- JavaScript:名前的部分型?(prototypeベースの継承)による型の判定
今回作ったコードはTypeScript Playgroundでも公開しているので、みなさんも触って確認してみてください
TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript