5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptで型の定義と実行時の型チェックをいい感じにやりたい

Last updated at Posted at 2022-02-19

以下のような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
runtypes
superstruct

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が良さそうかな〜と思いました。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?