以下のようなJSONをAPIが返すとして
{
"id": 12345 // required
"name": "qiitarou", // required
"age": 20 // optional
}
APIが返す型の定義をするのと、実行時にAPIがちゃんと想定どおりの型を返しているかチェックしたいじゃないですか
すると
type User = {
readonly id: number
readonly name: string,
readonly age?: number,
}
const isUser = (res: any): res is User =>
typeof res.id === 'number'
&& typeof res.name === 'string'
&& (res.age === undefined || typeof res.age === 'number')
みたいなことを書くことになると思いますが、
なんで、レスポンスがUser型であることを確かめたいだけなのに、こんな関数を書かなければならないんだ
User型を定義してあるんだから、それをうまいこと使えないのかな〜?
と思いますよね。
そんなときに便利なライブラリがあります。
TypeScriptの型でなくて、独自の型オブジェクトを定義してそこからTypeScriptの型として抽出したり、実行時の型のチェックを行えるというもの。
3つ紹介します。
io-ts
これが3つの中でDownload数、Star数が一番多いですね。
import * as t from 'io-ts'
const User = t.intersection([
t.readonly(t.type({
id: t.number,
name: t.string,
})),
t.readonly(t.partial({
age: t.number
}))
])
type UserType = t.TypeOf<typeof User> // 型の生成
/*
type UserType = Readonly<{
id: number;
name: string;
}> & Readonly<{
age?: number | undefined;
}>
が生成される。
*/
const user: UserType = { id: 1, name: 'qiitarou', age: 20 }
console.log(User.is(user)) // true
console.log(User.is({ id: 1, name: 'qiitarou' })) // true
console.log(User.is({ id: "1", name: 'qiitarou' })) // false
ちょっと特殊だなと思ったのはrequiredとoptionalのフィールドが存在する型を作りたかったらrequiredの型とoptionalの型をintersectionで合体させる部分
野暮ったく見えるよね。
あと、細かいけど、intersectionしているので、生成される型が以下のようになること。
type UserType = Readonly<{
id: number;
name: string;
}> & Readonly<{
age?: number | undefined;
}>
runtypes
import * as rt from 'runtypes';
const User = rt.Record({
id: rt.Number,
name: rt.String,
age: rt.Optional(rt.Number)
}).asReadonly()
type UserType = rt.Static<typeof User> // 型の生成
/*
type UserType = {
readonly id: number;
readonly name: string;
readonly age?: number | undefined;
}
が生成される。
*/
const user: UserType = { id: 1, name: 'qiitarou', age: 20 }
console.log(User.guard(user)) // true
console.log(User.guard({ id: 1, name: 'qiitarou' })) // true
console.log(User.guard({ id: "1", name: 'qiitarou' })) // false
こちらはOptional()が用意されているので、optionalのフィールドを簡単に作れます。
合体!とかさせないでoptionalフィールドを作れるので、TSで定義する型と同じ型を生成できました。
superstruct
import { is, object, number, string, optional, Infer } from 'superstruct'
const User = object({
id: number(),
name: string(),
age: optional(number())
})
type UserType = Infer<typeof User> // 型の生成
/*
type UserType = {
id: number;
name: string;
age?: number | undefined;
}
が生成される。
*/
const user: UserType = { id: 1, name: 'qiitarou', age: 20 }
console.log(is(user, User)) // true
console.log(is({ id: 1, name: 'qiitarou' }, User)) // true
console.log(is({ id: "1", name: 'qiitarou' }, User)) // false
これは、readonlyにする方法が見当たらなかったです。
まとめ
optionalの指定が簡単。readonlyもできる。ということで、個人的にはruntypesが良さそうかな〜と思いました。