🚨

TypeScriptの全てのasを撲滅するas-safelyというOSSを作ったゾォォ〜

2021/09/11に公開

はじめに

as-safelyというライブラリを作成しました。
このライブラリを使うと、型判定に失敗したときはその場で例外を投げてくれます。
危険なas(型アサーション)を撲滅し、真にタイプセーフなType Script環境を手に入れましょう。

https://github.com/YuitoSato/as-safely

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の強みとも言えますね※ただし使用者が天才の場合に限る)

※型アサーションの詳しい説明はこちら
https://typescript-jp.gitbook.io/deep-dive/type-system/type-assertion

ではどうすればいいのか?

それは型判定に失敗した瞬間、何かしらのハンドリングをすることです(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を撲滅しました

https://twitter.com/Yuiiitoto/status/1433453342223597570?s=20

react-hook-formやNext.jsのURLからクエリパラメータを抽出するときなどにas-safely使うことが多かったです。

const router = useRouter();
const userId = asSafely(router.query.userId, [isString, isUndefined]);

どう実装しているのか?

https://github.com/YuitoSato/as-safely/blob/master/src/as-safely.ts

メインの関数自体は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も募集しています!

ログラスは全職種のエンジニアを超絶募集しています!
型安全な環境で安心しながらチャレンジしたい方ぜひ!

https://job.loglass.jp/

Discussion