真に型安全なTypescriptを実現するには  ※こんなのネタだって自分でもわかってるんだ

真に型安全なTypescriptを実現するには ※こんなのネタだって自分でもわかってるんだ

logo 雑ポエム

投稿日: 2023年05月14日

Typescript の型そのものは実行時チェックしてくれないよ

Typescript は静的型付けだから安全!安全!と言われています。これらは tsc コマンド時に静的型チェックが行われますが、実行時には型によるチェックは行われません。以下のような例を見てみましょう。

  function add(a: number, b: number) {
 return a + b;
}
add(1, '2' as any); //  実行時に型エラーが発生しない
// output: "12"
  

このコードは、add 関数に number 型以外を渡そうとしても、エラーが発生せず、意図せぬバグを起こします。上記は極端な例ですが、例えば外部のライブラリなど、実装の詳細がわからないプログラムを利用する際、実装の詳細と型がずれてエラーが発生することは珍しいことではありません。

真の実行時型安全を得たいなら・・・

このような現象を防ぐためには実行時に型チェックする必要があります。

  function add(a: number, b: number) {
 if (typeof a === 'number' && typeof b === 'number') {
  return a + b;
 } else {
  throw new Error('a or b type is not number');
 }
}

add(1, '2' as any); // 実行時エラー
  

今回は単純な number 型なのでこれで十分です。しかし、これが大量のプロパティから成るオブジェクトになるとどうでしょう。プロパティ一つ一つに条件チェックをしていかなくてはなりません。

  function calc(obj: { a: number; b: number | undefined; c: number; title: string; author: string }) {
 if (
  typeof obj.a === 'number' &&
  (typeof obj.b === 'number' || typeof obj.b === 'undefined') &&
  typeof obj.c === 'number' &&
  typeof obj.title === 'string' &&
  typeof obj.author === 'string'
 ) {
  return `${obj.author}: ${obj.title} is written. ${obj.a} + ${(obj.b ?? 0) * obj.c}`;
 } else {
  throw new Error('obj is wrong type');
 }
}

calc({ a: 2, b: '5', c: 10, title: '偉大な発明', author: 'u-yas' } as any); // bがstringなのでエラー
  

愚直に一つ一つのプロパティのチェックを書こうとすると大量の条件チェックを書く必要があり、条件チェックのプログラム自体にバグが潜む可能性が増えます。

こういった大量の object の実行時チェックをするには zod のようなライブラリが便利です

  import { z } from 'zod';

const calcSchema = z.object({
 a: z.number(),
 b: z.number().optional(),
 c: z.number(),
 title: z.string(),
 author: z.string()
});

function calc(obj: z.infer<typeof calcSchema>): number {
 const o = calcSchema.parse(obj);
 return `${o.author}: ${o.title} is written. ${o.a} + ${(o.b ?? 0) * o.c}`;
}

calc({ a: 2, b: '5', c: 10, title: '偉大な発明', author: 'u-yas' } as any); // bがstringなのでエラー
  

だったらすべて Zod で書き直せばええやん!!


そうです!良く気づきましたね!すべての処理を zod で parse しちゃえばすべての処理で実行時型チェックを行ってくれるので、zod 自体にバグが無い限り、真の型安全なプログラムが書けますね!!

  import axios from "axios";
import { z } from "zod";

const userSchema = z.object({
  lastName: z.string(),
  firstName: z.number().min(18),
  email: z.string().email(),
});

const responseSchema = z.object({
  id: z.number(),
})
async function createUser(userData:  z.infer<typeof userSchema>): Promise< {
  const info =  userSchema.parse(userData);
  const response = await axios.post("<https://hoge.com/api/user>", info).then(responseSchema.parse);

  return response;
}

createUser({lastName: "u-yas", firstName: "blog", email: "<[email protected]>"}).then(res => { console.log(`your id is ${res.id}.`);}
  

完璧ですね!!

・・・・・

・・・


現実はなかなかうまくはいきません・・・

実行時チェックのオーバーヘッドが・・・ 例えば巨大な配列を実行時チェックしようとしましょう。

  import { z } from 'zod';

const say = console.info;
const bigNumArr = z.array(z.number());

const timer = (f: () => void, title: string) => {
 const startTime = performance.now();
 f();
 const endTime = performance.now();
 console.log(title, endTime - startTime);
};

timer(() => {
 const validated = bigNumArr.parse(Array(100).fill(1));
 say({ validated });
}, 'validated');
timer(() => {
 const noValidate = Array(100).fill(1);
 say({ noValidate });
}, 'noValidate');

validated;
2.6999999955296516;

noValidate;
0.20000000298023224;
  

※コードは以下の stackblitz に載せています。

https://stackblitz.com/edit/typescript-ryygv1?file=schemas%2FschemaWithDefaultType.ts

どうでしょう・・・パフォーマンスが 10 倍以上遅くなってしまいました・・・今回は要素数が 100 個でしたが、1000 個、10000 個と増やしていくと以下のような結果になりました。

console_zod_performance

すべてのコードを実行時チェックしようとすると一体どうなってしまうのでしょうね・・・・

まとめ

  • Typescript の型安全性はは tsc 時までの間のみ。実行時にチェックしてくれるわけではありません
  • 実行時のチェックは自分で書く必要があります。zod や yup みたいな実行時バリデーションのライブラリを使うととても簡単に実行時チェックをしてくれます
  • ただ、実行時チェックを大量に行うと行わない場合に比べてパフォーマンスが劣化します。
    • なので、とても大事な部分にのみピンポイントで利用しましょう。
    • 要はバラン s


では!

© 2023 u-yas All rights reserved.