【TypeScript】色々な絞り込みによる型安全(型ガード)

型を保証してアクセスしたい時や、型によって処理を分けたい時などに使用される絞り込みによる型ガード(Type Guard)についてです。
様々なパターンがありますのでそれぞれみていきます。

# in演算子
- 特定のオブジェクトに存在するプロパティを `in type guards` を用いることで型の絞り込みを行う

```typescript
type Person = { gender: string };
type PersonA = Person & { name: string };
type PersonB = Person & { age: number };

const judgePersonType = (person: PersonA | PersonB) => {
  // 引数で渡されたオブジェクトに 'age' が存在するかチェック
  if('age' in person) {
    console.log('This Person is PersonB');
    return;
  };
  // 引数で渡されたオブジェクトに 'name' が存在するかチェック
  if('name' in person) {
    console.log('This Person is PersonA');
    return;
  };
  console.log('etc...');
};
```

# instanceof演算子
- [instanceof type guards](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#instanceof-narrowing) を使った型の絞り込み
- インスタンスを検証する
- インスタンスのいずれかを引数として受け取る関数など

```typescript
class Animal {
  bark() {
    console.log('called bark');
  };
};

class Dog extends Animal {
  bowwow() {
    console.log('called bowwow');
  };
};

class Cat extends Animal {
  mew() {
    console.log('called mew');
  };
};

const action = (animal: Animal | Dog | Cat) => {
  // インスタンスが Dog の場合
  if (animal instanceof Dog) {
    animal.bowwow();
    return;
  }
  // インスタンスが Cat の場合
  if (animal instanceof Cat) {
    animal.mew();
    return;
  }
  // その他
  animal.bark();
}
```

# リテラル型のType Guard
- タグ付きUnion Types(別名: Discriminated Union / Tagged union)と呼ばれる
- 適用条件は以下の通り
    - 引数の全てが共通のプロパティを持っていること
    - その型が [Literal Types](https://www.typescriptlang.org/docs/handbook/literal-types.html) である場合

```typescript
const GENDER = {
  MALE: 'male',
  FEMALE: 'female',
  OTHER: 'other',
} as const;

type PersonA = { gender: typeof GENDER.MALE; name: string };
type PersonB = { gender: typeof GENDER.FEMALE; age: number };
type PersonC = { gender: typeof GENDER.OTHER; graduate: string };

const judgePersonType = (person: PersonA | PersonB | PersonC) => {
  switch(person.gender){
    case GENDER.MALE:
      return 'person type is PersonA';
    case GENDER.FEMALE:
      return 'person type is PersonB';
    case GENDER.OTHER:
      return 'person type is PersonC';
    default:
      return 'person type is never';
  }
}
```

# ユーザー定義のType Guard
- ユーザ定義型ガードは簡略すると 「開発者自身が何をもってその型であるかを定義できる」
- bool値を返す関数を作る
- `引数 is Type` のように記述し、匿名関数の戻り型アノテーションに利用する
- この関数を利用すると、与えれる引数が any型でも、その条件を通過したブロックではその型であると推論適用される

```typescript
type Person = { gender: string; [key: string]: any };
type PersonA = Person & { name: string };
type PersonB = Person & { age: number };

const isPersonA = (person: PersonA | PersonB): person is PersonA => {
  return person.name !== undefined;
};

const isPersonB = (person: PersonA | PersonB): person is PersonB => {
  return person.age !== undefined;
};

const getPersonType = (person: any) => {
  if (isPersonA(person)) {
    return 'A';
  }
  if (isPersonB(person)) {
    return 'B';
  }
  return 'unknown';
};

const personA: PersonA = {
  gender: 'male',
  name: 'Tom',
};

const personType = getPersonType(personA);

console.log(personType); // 'A'
```

# Array.filterで型を絞り込む
- 通常 Array.filter は型を絞り込めない
- しかし、ユーザー定義ガード節が付与された関数を併用することで絞り込める

```typescript
const GENDER = {
  MALE: 'male',
  FEMALE: 'female',
  OTHER: 'other',
} as const;

type Gender = typeof GENDER[keyof typeof GENDER]; // "male" | "female" | "other"

type Person = { name: string };
type PersonA = Person & { gender: Gender };
type PersonB = Person & { graduate: string };

const persons: (PersonA | PersonB)[] = [
  { name: 'Tom', gender: 'male' },
  { name: 'Brown', graduate: 'NY' },
];

const filterPerson = (person: PersonA | PersonB): person is PersonB => {
  return 'graduate' in person;
};

// PersonBに一致するオブジェクトだけを取得する
const filteredPersons = persons.filter(filterPerson);

console.log(filteredPersons); // [{"name": "Brown", "graduate": "NY"}]
```

以上となります。
お読み頂き有難うございました。

# 参考文献

https://book.mynavi.jp/ec/products/detail/id=104703

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard