Typescript で 型(構造体) の一致を判定する (Class, Interface, Type)

# やりたいこと

とあるオブジェクトが `Class`、 `Interface` または `Type` の型を満たすのか判定したい。

## コードで

こんな感じのイメージ

```ts
class Target {
    id: number = 0;
    name: string = "taro";
}

const obj = {
    id: 3,
    name: "jiro",
}

type isTarget = (arg: unknown) => boolean;

console.log(isTarget(obj))
// => true
```

# ポイント

## 1. typeof

### [**typeof**](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof) で判定できるのは `object | boolean | number | string | undefined | function` 等で、構造の判定には使えない

今回は、Class, Interface, Type の判定ということで、これだけで直接判定することはできない。

## 2. instanceof

### [**instanceof**](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof) は、クラスインスタンス(例: `new Target()`)の判定かつ、判定先がクラス(例: `class Target {}`)の時に使用できる。

> instanceof 演算子は、オブジェクトが自身のプロトタイプにコンストラクタの prototype プロパティを持っているかを確認します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof

ここからわかるように、オブジェクトの構造自体を直接検証しているわけではないことに注意する。

## 3. ユーザー定義型ガード

### **1, 2** が使えない構造の一致判定は [**ユーザー定義型ガード**](https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard) の仕組みを使うべし

> JavaScriptには非常に豊富な実行時の解析サポートが組み込まれていません。単純なJavaScriptオブジェクトだけを使用している場合(構造型を使用する場合)、 instanceofまたはtypeofにアクセスすることさえできません。
https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard

オブジェクトの内部構造を検証して型の一致を確認したい場合、この方法を使うことになる。

# 判定してみる

テストツールには [Jest](https://jestjs.io/) を使用する。

## Class の判定

instanceof を使う。

```ts
describe("型検証", () => {
    // 検証先はクラス
    class Target {
        id: number = 0;
        name: string = "taro";
    }
    it("ポイント2の方法が使える", () => {
        const target = new Target();
        expect(target instanceof Target).toBeTruthy();
    });
}
```

## Interface, Type, Class の判定

**ユーザー定義型ガード** と **typeof** を組み合わせて検証する。

### 型の定義

まずは、検証を行いたい型を定義する。

```ts
// Class の他に、Interface, Type で書ける
class Target {
    id: number =0;
    name: string = "taro";
}
// interface Target {
//     id: number;
//     name: string;
// }
// type Target = {
//     id: number;
//     name: string;
// }
```

### ユーザー定義型ガード の作成

構造体の型を検証するための **ユーザー定義型ガード** を実装する。

- プロパティが存在することの確認
- プロパティの型一致

等を行うことがポイントとなる。

```ts
const isTarget = (arg: unknown): arg is Target =>
    typeof arg === "object" &&
    arg !== null &&
    // as Target で型の予測を効かせて typo を防ぐ
    typeof (arg as Target).id === "number" &&
    typeof (arg as Target).name === "string";
```

### 検証してみる

#### Valid

```ts
// オブジェクト作成
const validObj = {
    id: 1,
    name: "saburo",
};
```

```ts
// 検証
it("valid", () => {
    expected(isTarget(validObj)).toBeTruthy();
    // ここから validObj を Target 型のオブジェクトとして型推論が使える
    expect(validObj.id).toBe(1);
    expect(validObj.name).toBe("saburo");
})
```

#### InValid

```ts
// オブジェクト作成
const invalidObj = {
    a: "invalid",
    b: "jiro",
};
```

```ts
// 検証
it("invalid", () => {
    expect(isTarget(validObj)).toBeFalsy();
})
```

# さいごに

これで、Typescript の型とまた一歩仲良くなれた。

# 使用したテストの全体

```ts
describe("型検証", () => {
    // Class の他に、Interface, Type で書ける
    class Target {
        id: number = 0;
        name: string = "taro";
    }
    // interface Target {
    //     id: number;
    //     name: string;
    // }
    // type Target = {
    //     id: number;
    //     name: string;
    // }
    describe("structure match", () => {
        const isTarget = (arg: unknown): arg is Target =>
            typeof arg === "object" &&
            arg !== null &&
            typeof (arg as Target).id === "number" &&
            typeof (arg as Target).name === "string";
        describe("valid", () => {
            const validObj = {
                id: 1,
                name: "taro",
            };
            const returnTarget = (target: Target): Target => target;
            it("instanceof(Target が Class の場合には、コンパイルエラーとなる)", () => {
                expect(returnTarget(validObj) instanceof Target).toBeFalsy();
            });
            it("guard", () => {
                expect(isTarget(validObj)).toBeTruthy();
            });
        });
        describe("invalid", () => {
            const invalidObj = {
                id: true,
                name: "fuga",
            };
            it("instanceof(Target が Class の場合には、コンパイルエラーとなる)", () => {
                expect(invalidObj instanceof Target).toBeFalsy();
            });
            it("guard", () => {
                expect(isTarget(invalidObj)).toBeFalsy();
            });
        });
        describe("extended", () => {
            const extendedValidObj = {
                id: 2,
                name: "saburo",
                c: "fuga",
            };
            it("instanceof(Target が Class の場合には、コンパイルエラーとなる)", () => {
                expect(extendedValidObj instanceof Target).toBeFalsy();
            });
            it("guard", () => {
                expect(isTarget(extendedValidObj)).toBeTruthy();
            });
        });
    });
});
```

# 参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/instanceof
https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard