@total-typescript/ts-resetを導入してTypeScriptの痒いところを掻く
TypeScriptの痒いところ
TypeScriptの型システムは強力ですが、意図した型を推論できないケースもあり、any型や型アサーションを使わざるを得ないことがあります。
しかし、これらを使用すると型安全性を失い、本来コンパイルエラーとして検出されるべき問題が見逃されてしまい、実行時エラーを引き起こす原因になります。
コンパイルエラーは開発時に修正できますが、実行時エラーは本番環境で発生すると大きな影響を及ぼす可能性があり、回避したいものです。
@total-typescript/ts-resetについて
@total-typescript/ts-reset
というライブラリでは、以下のような機能があります。
- JSON.parseの返り値をunknownとしてくれる
- .filter(Boolean)で、型からもfalsyな値を取り除いてくれる
- includes元の配列に存在しない型を渡せる
それぞれの機能が解決する課題を紹介します。
導入
- インストール
npm i -D @total-typescript/ts-reset
- 設定をする
src配下に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からお問い合わせいただけると幸いです。
Discussion