TypeScriptの全てのasを撲滅するas-safelyというOSSを作ったゾォォ〜
はじめに
as-safelyというライブラリを作成しました。
このライブラリを使うと、型判定に失敗したときはその場で例外を投げてくれます。
危険なas(型アサーション)を撲滅し、真にタイプセーフなType Script環境を手に入れましょう。
const str1: string = asSafely('1' as unknown, isString);
// => OK
const str2: string = asSafely(1 as unknown, isString);
// => 1はstringではないので実行時にエラーを投げる。
const str3: string = asSafely(1, isString);
// => そもそも1はstring型ではないのでnumber型だとわかっている場合はコンパイル時にエラーを吐く
const strOrUndefined1: string | undefined = asSafeley(1 as unknown, isString, () => undefined);
// => undefined
// => 型チェックに失敗してもエラーを投げずにundefinedに変換する
真なるタイプセーフのために
みなさんasは使ってますか?
「画面的にはここは絶対undefinedこないから、、」
const userId: string = getLoginUserId() as string;
「any型って書いてあるけどここはHoge型だよね(型定義のないJSライブラリを使う)」
const hoge: Hoge = someJSLibrary.getHoge() as Hoge;
はいそれ、型に嘘をついています
よほどの天才でない限り、自分の頭脳とJavaScript製のライブラリを信じてはいけません。
as(以後型アサーション)が非常に難しいのは型アサーション自体が実行時に何もエラーを出してくれないことです。
しかもJavaScriptは型がおかしくてもエラー出さずになんとかしてくれる事が多いです(例えば文字列と数字の四則演算とか)。
型がおかしくても実行時のJavaScriptの処理はそのままつつがなく進み、意味わからない結果を返したりします。
const unknown = '1' as unknown;
const num: number = unknown as number;
const num2: number = num + 3;
console.log(num2 * 3);
// => 12とみせかけて39!
上記の例は非常にシンプルな例ですが、実際のコードの場合は複雑なものも多いです。
カジュアルな型アサーション(嘘)の積み重ねはTypeSciriptのプロジェクトを容易に破壊します。
(逆に言うとこういうゆるさがTypeScriptの強みとも言えますね※ただし使用者が天才の場合に限る)
※型アサーションの詳しい説明はこちら
ではどうすればいいのか?
それは型判定に失敗した瞬間、何かしらのハンドリングをすることです(undefinedにしてもいいし、エラーを投げてもいいと思います)
const unknown = '1' as unknown;
if (typeof unknown !== 'number') {
throw new Error('数字ではない!')
}
const num: number = unknown;
const num2: number = num + 3;
console.log(num2 * 3);
でもめんどくさい。エラーを投げるのは式として扱えないので以下のようにワンライナーでも書けないのです。
const num: number = typeof unknown !== 'number' ? unknown : throw new Error('数字ではない!');
これをワンライナーで書けるようにしたのが as-safely というライブラリです。やった!!!
【型判定に失敗する例】
import { asSafely, isNumber } from 'as-safeley';
const unknown = '1' as unknown;
const num: number = asSafely(unknown, isNumber);
// => ここでエラーを投げてくれるのでエラー調査が簡単になる
const num2: number = num + 3;
console.log(num2 * 3);
【型判定に成功する例】
import { asSafely, isNumber } from 'as-safeley';
const unknown = 1 as unknown;
const num: number = asSafely(unknown, isNumber);
// => 型アサーションに成功するのでそのままnumber型として安全に処理が進む
const num2: number = num + 3;
console.log(num2 * 3);
スマートにかけて最高!
ちなみに2つまでならリテラル型に対応できます。
import { asSafely, isNumber, isUndefined } from 'as-safeley';
const unknown = 1 as unknown;
const numOrUndefined: number | undefined = asSafely(unknown, [isNumber, isUndefined]);
そしてどうなった?
ログラスではこの技術を導入していて 100個くらいのasを撲滅しました
react-hook-formやNext.jsのURLからクエリパラメータを抽出するときなどにas-safely使うことが多かったです。
const router = useRouter();
const userId = asSafely(router.query.userId, [isString, isUndefined]);
どう実装しているのか?
メインの関数自体は20行ほどの小さなライブラリです。
const asSafely = <RESULT extends TARGET, TARGET = unknown, OR_ELSE = RESULT, RESULT2 = RESULT>(
obj: TARGET,
condition:
| ((obj: unknown) => obj is RESULT)
| [(obj: unknown) => obj is RESULT, (obj: unknown) => obj is RESULT2],
orElse?: (obj: TARGET) => OR_ELSE
): RESULT | RESULT2 | OR_ELSE => {
if (!Array.isArray(condition) && condition(obj)) {
return obj as RESULT;
}
if (Array.isArray(condition) && condition.length > 0 && condition.some((c) => c(obj))) {
return obj as RESULT | RESULT2;
}
if (orElse != null) {
return orElse(obj);
}
throw new Error(
`type assertion is failed. object type: ${typeof obj}. object keys: ${obj && Object.keys(obj)}`
);
};
可能な限りタイプセーフにするためTypeScriptの型を頑張ってます。
シンプルにすると
const asSafely = (obj, condition, orElse?): RESULT | RESULT2 | OR_ELSE;
です。 RESULT
は最終的に型アサーションしたい型で、 OR_ELSE
は型判定に失敗したときの型です。デフォルトではエラーを投げるのでここはRESULT型となります。
RESULT2
は先程の2つまで型リテラルができるという話で、 conditionを配列で受け取ったときは RESULT | RESULT2
で返すような形になります(conditionを配列で指定しないとRESULT2
はデフォルトでRESULT
を型にとる)。
第2引数の condition
は (obj: unknown) => obj is Result
なので自分で型判定の関数をつくって入れ込むことも可能です。
type User = {
name: string
}
const isUser = (obj: uknown): obj is User => typeof (obj as User).name === 'string'
const unknown = {
name: Taro
} as unknown;
const user: User = asSafely(unknown, isUser);
おわりに
とまあ、詳しい説明はこの辺にしておきます。
また生まれて間もないライブラリですが、懇意にしていただけると嬉しいです!!!
PRも募集しています!
ログラスは全職種のエンジニアを超絶募集しています!
型安全な環境で安心しながらチャレンジしたい方ぜひ!
Discussion