6
6

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 3 years have passed since last update.

io-tsでJSONの型検査をする

Last updated at Posted at 2019-11-18

TypeScript内でJSONを使うことは多いが悩みの一つがJSON.parse()の戻り値はanyになって型が不明になることである。

const x: number =  JSON.parse(json);

とすると型はつくにはつくが特に検査するわけではないので

const x: number =  JSON.parse('"abc"');
const y = x.a * 2 // => NaN になる

とやってランタイムで滅茶苦茶になってしまうのは防げない。

io-ts (https://github.com/gcanti/io-ts) を使うとJSONの型検査ができる。しかしこれはTypeScriptで関数型プログラミングをするライブラリのfp-ts(https://github.com/gcanti/fp-ts) をベースにしているのでこちらの知識が必要になる。

ライブラリの中にはnumberやstringの検査するものと組み合わせてobjectを作ったりするものがある。

以下Jestのテストコードでの使用例。

import * as t from "io-ts";
import { isRight, map, right } from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/pipeable";

describe("JSON.parse()のみ", () => {
  test("stringをobjectと処理するとundefined", () => {
    const x = JSON.parse(`"a"`);
    expect(x.b).toBeUndefined();
  });
  test("存在しない属性はundefined", () => {
    const x = JSON.parse(`{"a":123}`);
    expect(x.b).toBeUndefined();
  });
  test("stringをnumberとして処理するとNaN", () => {
    const x: number = JSON.parse(`"a"`);
    expect(x * 2).toBeNaN();
  });
});

describe("io-tsを使う", () => {
  test("stringはnumberではない", () => {
    const x = t.number.decode(JSON.parse(`"a"`));
    expect(isRight(x)).toBeFalsy();
  });
  test("numberはstringではない", () => {
    const x = t.string.decode(JSON.parse(`123`));
    expect(isRight(x)).toBeFalsy();
  });

  test("オブジェクトの検査", () => {
    const X = t.type({ a: t.number });
    const x = X.decode(JSON.parse(`{"a":123}`));
    expect(x).toStrictEqual(right({ a: 123 }));
  });
  test("オブジェクトのプロパティが一致しない", () => {
    const X = t.type({ x: t.number });
    const x = X.decode(JSON.parse(`{"a":123}`));
    expect(isRight(x)).toBeFalsy();
  });
  test("余分なプロパティがあっても良い", () => {
    const X = t.type({ a: t.number });
    const x = X.decode(JSON.parse(`{"a":123,"b":456}`));
    expect(x).toStrictEqual(right({ a: 123, b: 456 }));
  });
  test("exactを使うと余分なプロパティは消える", () => {
    const X = t.exact(t.type({ a: t.number }));
    const x = X.decode(JSON.parse(`{"a":123,"b":456}`));
    expect(x).toStrictEqual(right({ a: 123 }));
  });
  test("unionの例", () => {
    const X = t.union([
      t.type({ x: t.literal("number"), a: t.number }),
      t.type({ x: t.literal("string"), a: t.string })
    ]);
    type XType = t.TypeOf<typeof X>; // 型情報が取れる
    const num = X.decode(JSON.parse(`{"x": "number", "a":123}`));
    const str = X.decode(JSON.parse(`{"x": "string", "a":"xyz"}`));

    const f = (a: XType) => {
      switch (a.x) {
        case "number":
          return a.a * 2;
        case "string":
          return a.a.length;
      }
    };
    expect(pipe(num, map(f))).toStrictEqual(right(246));
    expect(pipe(str, map(f))).toStrictEqual(right(3));
  });
});

6
6
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
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?