🚀

一日一処: TypeScriptで値の型を判定するための処理

2024/01/31に公開

値の型

よく関数の返却で、anyの配列などを返却しないといけない場合もある。このとき、受け取り側は、適切な型として取り扱いたいが、内容を確認せずasを用いて、型のキャスティングを行い、知らぬ存ぜぬで、プログラムを書く。非常に不適切な行いだ。

function getData(): any[] {
  return [1, 'bob', 'japan']
}

type User = [id: number, name: string, country: string]
const data = getData() as User

data.forEach((v) => console.log(v))
// 1
// 'bob'
// 'Japan'

型を判定

少々処理が大変だが、簡単な話、判定を行う関数の返却値を少しだけ変えてあげることが大切だ。まずは、通常の判定を行う関数を作ってみる。

function getData(): any[] {
  return [1, 'bob', 'japan']
}

function validate(data: any[]): boolean {
  return typeof data[0] === 'number' &&
         typeof data[1] === 'string' &&
         typeof data[2] === 'string'
}

type User = [id: number, name: string, country: string]
const data = getData()

if (validate(data)) {
  (data as User).forEach((v) => console.log(v))
  // 1
  // 'bob'
  // 'Japan'
}

この様に、検証用の関数を追加し、適切であることを確認したあとに、真偽値によって、結果を返せば、文句なく、キャストすることができるだろう。それでも、asを用いてることがあまり好ましくないように見える。
よって、最終的に、掲題の実現で、この様に書く。

function getData(): any[] {
  return [1, 'bob', 'japan']
}

function validate<T extends any[]>(data: any[]): data is T {
  return typeof data[0] === 'number' &&
         typeof data[1] === 'string' &&
         typeof data[2] === 'string'
}

type User = [id: number, name: string, country: string]
const data = getData()

if (validate<User>(data)) {
  data.forEach((v) => console.log(v))
}

このように検証用の関数の返却値をdata is T(type predicate)とすることで、検証後のdataは自動的に型がUserとなる。これは、forEachを使った例だが、オブジェクトを同様に用いた場合、キー名を適切に用いることができる(エディターによるサジェストもされる)。
これを更に、汎用的にしてみると、このような形になるだろうか。人によって、こだわりはあると思うが、キャストを行わずに、適切な検証を行ってほしい。

function getData(): any[] {
  return [1, 'bob', 'japan']
}

function validate<T extends any[]>(data: any[], rule: string[]): data is T {
  return data.length === rule.length
    ? data.every((_, i) => typeof data[i] === rule[i])
    : false
}

type User = [id: number, name: string, country: string]
const rule = ['number', 'string', 'string']
const data = getData()

if (validate<User>(data, rule)) {
  data.forEach((v) => console.log(v))
}

Discussion