🕌

@total-typescript/ts-resetを導入してTypeScriptの痒いところを掻く

2024/11/07に公開

TypeScriptの痒いところ

TypeScriptの型システムは強力ですが、意図した型を推論できないケースもあり、any型や型アサーションを使わざるを得ないことがあります。
しかし、これらを使用すると型安全性を失い、本来コンパイルエラーとして検出されるべき問題が見逃されてしまい、実行時エラーを引き起こす原因になります。

コンパイルエラーは開発時に修正できますが、実行時エラーは本番環境で発生すると大きな影響を及ぼす可能性があり、回避したいものです。

@total-typescript/ts-resetについて

@total-typescript/ts-resetというライブラリでは、以下のような機能があります。

  • JSON.parseの返り値をunknownとしてくれる
  • .filter(Boolean)で、型からもfalsyな値を取り除いてくれる
  • includes元の配列に存在しない型を渡せる

それぞれの機能が解決する課題を紹介します。

導入

  1. インストール
npm i -D @total-typescript/ts-reset
  1. 設定をする

src配下にreset.d.tsを配置します。

reset.d.ts
// すべての機能を使用する場合
import '@total-typescript/ts-reset';

// JSON.parseのみ
import '@total-typescript/ts-reset/dist/json-parse';

// .filter(Boolean)のみ
import '@total-typescript/ts-reset/dist/filter-boolean';

// includesのみ
import '@total-typescript/ts-reset/dist/array-includes';

JSON.parseの返り値をunknownとしてくれる

課題

通常、JSON.parseを使用すると、any型が返ってきます。
any型では、存在しないプロパティへのアクセスが可能となり、型チェックが完全に無効化された状態です。そのため、型安全性が失われ、意図しないエラーが発生するリスクが高まります。

解決してくれる点

@total-typescript/ts-resetを使用すると、JSON.parseの結果がunknown型で返されるようになります

unknown型では、存在しないプロパティにアクセスしようとするとコンパイルエラーが発生するため、any型よりも安全です。

では、unknown型の場合にどのようにプロパティにアクセスするかというと、型ガードを使用します。

例えば、以下のように実装できます。

// 期待されるUser型
interface User {
  name: string;
  age: number;
}

// User型であることを確認する型ガード
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    'age' in value
  );
}

// JSON.parse後の型チェック
const jsonString = '{"name": "Alice", "age": 25}';
const parsedData = JSON.parse(jsonString);

if (isUser(parsedData)) {
// このブロックではアクセスできる
  console.log(parsedData.name);
  console.log(parsedData.age);
} else {
// このブロックではアクセスできない
  console.log('Invalid User data');
}

型ガードがない状態で parsedData が User 型でない場合、本番環境などで実行時エラーが発生するリスクがあります

.filter(Boolean)で、型からもfalsyな値を取り除いてくれる

課題

配列に対して .filter(Boolean) を使用すると、配列から falsy な値を取り除いてくれます。
しかし、型推論上は falsy な値を取り除かず、実行前の型がそのまま推論されます。

例えば、以下のようにすると、cleanedData は false | "" | 1 | 2 | 3 | 4 | null | undefined と推論されてしまいます。

const data = [1, null, 2, undefined, 3, '', 4, false] as const;
const cleanedData = data.filter(Boolean);

解決してくれる点

@total-typescript/ts-reset を使用すると、(1 | 2 | 3 | 4)[] と推論してくれます。
これにより、値と型に差異がなくなり、型安全性が向上します。

false | "" | 1 | 2 | 3 | 4 | null | undefined のように推論される必要があるケースはあまりないと思うので、導入して損がないと思います。

includes元の配列に存在しない型を渡せる

課題

例えば、以下の場合にはコンパイルエラーが発生します。

const value: string = "sample";

const array = ['hoge', 'fuga'] as const;
type Array = (typeof array)[number];

array.includes(value); // コンパイルエラー!

arrayは('hoge' | 'fuga')[]型である一方、valueがstring型であるために発生しています。

そのため、以下のように型アサーションを用いて対応することになります。

array.includes(value as Array);

上記のように、asを使って型アサーションを行うことは、TypeScriptの型システムを無視することになり、型安全性が低下します。できる限り避けたい書き方です。

解決してくれる点

@total-typescript/ts-resetを使用すると、includes元の配列に存在しない型の値を渡せるので、型アサーションを使わずにすみます。

例えば、以下のように型ガードを使用することで、型アサーションを使わずにsetArrayのstateを更新できます。

const array = ['hoge', 'fuga'] as const;
type Array = (typeof array)[number];

const [array, setArray] = useState<Array>('hoge');

// 型ガード
function isArray(value: string): value is Array {
  return frames.includes(value);
}

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const value = e.target.value;
// @total-typescript/ts-resetを使用しない場合、型ガードを使わずにsetArray(value as Array);と書く必要があった
  if (isArray(value)) {
    setArray(value); 
  }
};

最後に

弊社では、多くのプロダクトでバックエンドおよびフロントエンドの両方をTypeScriptで開発しています。
今回のような型安全性を高めるための取り組みを、エンジニアが試行錯誤しながら進めています。

現在、カジュアル面談を実施中ですので、下記のHPからお問い合わせいただけると幸いです。

https://x-point-1.net/

エックスポイントワン技術ブログ

Discussion