🧢

TypeScriptの型ガードをちゃんと使おう

2022/05/29に公開約4,800字2件のコメント

はじめに

みなさんは、型ガードを有効活用できていますか?もしかしたら、型ガードを利用せずに type assertion で、 never型 や unknown型 を無理やり割り当てて解決してないでしょうか?
今回は、みなさんもぜひ型ガードを有効に利用して、型安全なTypeScriptコーディングができればなあと思い本記事を書いていきます。

TypeScriptの型ガード(Type Guard)について

Type Guardを使用すると、条件ブロック内のオブジェクトの型を制限することができます。

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

これだけ言われてもどう使うかはあまりわからないですね。

具体例を下記にコードで示していきます。

type DogBreed = 'Shiba Inu' | 'Golden retriever' | 'Chihuahua'
type Dog = {
  readonly dogBreed: DogBreed,
  readonly name: string
}
const shibaTaro = {
  dogBreed: 'Shiba Inu',
  name: 'Shiba Taro'
} as const
type ShibaTaro = typeof shibaTaro

const getShibaTaro = (dog: Dog): ShibaTaro | undefined => {
  if (dog.dogBreed === shibaTaro.dogBreed && dog.name === shibaTaro.name) {
    return dog // 型エラー
  }
  return undefined
}

この記述では、getShibaTaro(arg) の戻り値の型である ShibaTaro のプロパティの dogBreed の型定義に対して、引数として渡された dog のプロパティである、dogBreed の型定義の DogBreed に 'Shiba Inu' だけではなく、他の犬種も含まれているため、型の整合性が合わなくなりエラーとなります。
本来であれば、dog.dogBreed に対して 'Shiba Inu' のみになるように型ガードを行う必要があります。またこの際に、return dog as ShibaTaroとするのは、型安全ではないのでできれば避けたいです。

型ガードをちゃんと行う

具体例で型ガードがどういう時に必要かをおわかりいただけたと思います。
本来であれば、ユーザ定義型ガードを利用して下記のように、ShibaTaro であると確定させる必要があります。

// 上述の定義は省略
const isShibaTaro = (dog: Dog): dog is ShibaTaro => dog.dogBreed === shibaTaro.dogBreed && dog.name === shibaTaro.name
const getShibaTaro = (dog: Dog): ShibaTaro | undefined => {
  if (isShibaTaro(dog)) {
    return dog // ShibaTaro型
  }
  return undefined
}

型ガードを応用して汎用関数を作ろう

型ガードについて、おおまかにはどういうものかを理解いただけたと思います。
以下では、個人的に有用かも?と思って作成した型ガード用の汎用関数について紹介できればなと思います。

紹介する汎用関数

型ガードが必要になる場面は、具体例の時のような独自で定義した型のときもそうですが、
基本的には unknown型 を受け取る時などに、Array、Object、Record型 の KeySignature、undefined などの例外条件を型安全になるようにしたいと思って型の判定が必要になるときだと思います。

例えば、APIからレスポンスされた、getApiResponse(arg: unknown)のような関数があった場合、引数に設定される値が、渡されてきた時点では、Objectか、Arrayか、などの判断はつきません。渡されたデータの振る舞いに応じて、処理を変える必要もある場合が少なくないです。
そんな時に下記で紹介するようなユーザー定義型ガードを応用して作成できる汎用関数が役に立つかなと思います。

/** typeUtils.ts */
// conditional typeを利用して、指定した型の追跡が可能になります。
type S<T, K> = T extends K ? T : K

// 型判定用の定義ですが、定義自体は必須ではありません。
const typeOf = {
  isArray: <T>(array: T): boolean => (
    typeof array === 'object' && Array.isArray(array)
  ),
  isObject: <T>(obj: T): boolean => (
    typeof obj === 'object' && !Array.isArray(obj) && obj != null
  ),
  isString: <T>(value: T): boolean => (typeof value === 'string'),
  isUndefined: <T>(value: T): boolean => value === undefined,
}

/**
 * NOTE: 型ガード用の関数定義(exportなし)
 */
const isTypeGuardOf = {
  isArray: <T>(array: unknown): array is T => typeOf.isArray(array),
  isObject: <T>(obj: unknown): obj is T => typeOf.isObject(obj),
  isString: <T>(value: unknown): value is T => typeOf.isString(value),
  isUndefined: (value: unknown): value is undefined => typeOf.isUndefined(value),
}

/**
 * 型ガード用の汎用関数
 * guard${Type}<T>(arg)を利用することで、Tで指定した型が戻り値であることを担保しながら型ガードを行えます。
 */
const typeGuardOf = {
  /**
   *  型ガードをしつつ、引数が必ずArrayであること担保します。
   *  また、返却する[]がundefined[]型である理由は、unknwon[]を返却してしまうと、array[0]: unknownとなってしまい、実態と乖離してしまうためです。
   */
  guardArray: <T>(array: unknown): S<T, T | undefined[]> | undefined[] => (
    isTypeGuardOf.isArray<S<T, []>>(array) ? array : []
  ),
  /**
   * 型ガードをしつつ、引数が必ずObjectであること担保します。
   */
  guardObject: <T>(obj: unknown): S<T, Object & Record<keyof T, T[keyof T]>> | Object => (
    isTypeGuardOf.isObject<S<T, Object & Record<keyof T, T[keyof T]>>>(obj) ? obj : {} as Object
  ),
  /**
   * KeySignatureの場合は、string型であることも同時に示せるようになります。
   */
  guardString: <T>(value: unknown): S<T, T & string> | '' => (
    isTypeGuardOf.isString<S<T, T & string>>(value) ? value : ''
  ),
  guardUndefind: (value: unknown): undefined => (
    isTypeGuardOf.isUndefined(value) ? value : undefined
  ),
}

export {
  typeOf,
  typeGuardOf,
}

具体例に適用してみよう

紹介した汎用関数を利用する際の一例として、 typeGuardOf.guardArray<T>(arg)の利用例を以下で書いていきます。

import { typeGuardOf, isTypeGuardOf } from './typeUtils.ts'

const getApiResponse = (arg: unknown) => {
  if (isTypeGuardOf.isString<string>(arg)) {
    const matchArray = arg.match(/abc/) // null | RegExpMatchArray
-   const matchString = matchArray[0] // 型エラー
+   const matchString = typeGuardOf.guardArray<RegExpMatchArray>(matchArray)[0] // 型エラー解消
    console.log(matchString) // undefined | string
  }
}

この例は本来、matchArray に格納している String.prototype.match() の型が  null | RegExpMatchArray であるために、matchArray は Arrayであると確定していないため、null を例外処理した上で、matchArray[0] にアクセスする必要があります。
これに型ガードを利用した汎用関数を上記のよう利用することで、型エラーにならず array[0] にアクセスできるようになるため、不必要な例外処理が必要なくなります。
また、必要であれば、 typeGuardOf.guardArray<RegExpMatchArray>(matchArray)[0] が undefined かを判定して、例外処理をすることも可能になりますね。

おわりに

久しぶりの投稿になりましたが、この記事がみなさんのTypeScript生活を少しでも豊かにできると嬉しいです。もし間違いやご指摘、改善点などあれば喜びますのでコメントお願いします。

Discussion

補足ですが、引数の型Tは、指定しなかった場合自動でunknown型に推論されるのでそれを利用してます。

個人的に指摘を受けて記事の修正を行いました。

ログインするとコメントできます