mongolyyのブログ

開発(Javascript, Typescript, React, Next.js)や開発手法(スクラム, アジャイル)、勉強したことについて色々書ければと。

【TypeScript】2つの型システムが混在することを意識すべしという話

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のドキュメントを読まれるといいと思います。

developer.mozilla.org

今回、InvalidError はprototypeが InvalidError になっているので、instanceofはtrueとなります。
しかし、標準のErrorのprototypeチェーンには Objectnull しかいないので、instanceofはfalseとなります。

おわりに

TypeScriptで型のチェックと言っても次の2つのチェックタイミング、型システムが混在することを意識しなければなりません。

  • トランスパイル時
    • TypeScript:構造的部分型によるチェック
  • 実行時
    • JavaScript:名前的部分型?(prototypeベースの継承)による型の判定

今回作ったコードはTypeScript Playgroundでも公開しているので、みなさんも触って確認してみてください

TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript