ts-reset は、歴史的経緯によって生じている TypeScript のイケてない部分を矯正してくれるライブラリです。
これを使えば、 JSON.parse の戻り値を any から unknown にしたりといった、地味だけど とても強力な恩恵を受けることができます。
さて、その ts-reset の機能の一つとして、 Array の includes の型改善があります。
以下のコードをご覧ください:
const modes = ['dev', 'stg', 'prd'] as const; const mode: string = process.env.MODE ?? '(undefined)'; if (!modes.includes(mode)) { throw new Error(`unknown mode: ${mode}`); } // do something with mode...
一見なんの問題も無さそうなコードですが、このコードは ts-reset 未導入の状態では modes.includes(mode) の部分で型エラーになります:
Argument of type 'string' is not assignable to parameter of type '"dev" | "stg" | "prd"'.
これは TypeScript の Array<T> (または ReadonlyArray<T> )の includes が引数として T 型を要求するためです(この場合 T は string ではなく更に具体的な型 'dev' | 'stg' | 'prd' になっているため、 string を渡すと型エラーになってしまう)。
ts-reset を導入すれば、この不便な挙動を修正することが出来ます。
更に、 ts-reset のかつてのバージョンでは、これに伴い型ガードも行ってくれていました。 すなわち:
const modes = ['dev', 'stg', 'prd'] as const; const mode: string = process.env.MODE ?? '(undefined)'; if (!modes.includes(mode)) { throw new Error(`unknown mode: ${mode}`); } // ここで mode は 'dev' | 'stg' | 'prd' と推論される switch (mode) { case 'dev': // do something; break; case 'stg': // do something; break; case 'prd': // do something; break; default: // should not get here mode satisfies never; }
こんな感じで、 includes が true になった mode に対しては、それが 'dev' か 'stg' か 'prd' のいずれかであることを推論してくれていたのです。
しかし、現行の ts-reset (バージョン 0.4.2 以降)では、この挙動は削除されました。
その理由として、公式では以下のコードを挙げて、この挙動がおかしいことが説明されています:
type Code = 0 | 1 | 2; type SpecificCode = 0 | 1; const currentCode: Code = 0; // Create an empty list of subset type const specificCodeList: ReadonlyArray<SpecificCode> = []; // This will be false, since 0 is not in [] if (specificCodeList.includes(currentCode)) { currentCode; // -> SpecificCode } else { // This branch will be entered, and ts will think z is 2, when it is actually 0 currentCode; // -> 2 }
要するに、 includes を呼び出す配列が T の各要素を全て網羅していない場合に、 if 〜 else の else 周りで意図しない挙動が起きるから、というものです。
この変更は極めて妥当なものであり、この変更を批判するつもりは筆者にはありません。
仮に型ガードを行いたい場合には、独立した関数に分けた方が確実ですしね:
const modes = ['dev', 'stg', 'prd'] as const; type Mode = modes[number]; function isMode(x: string): x is Mode { return modes.includes(x); // ts-reset 前提 }
が、以前の型ガードの挙動も それはそれで便利だったので、以前の挙動を再現する関数を紹介して、記事を終わらせたいと思います。
// ts-reset なしでも動く const includes = <A extends readonly unknown[]>(arr: A, x: unknown): x is A[number] => arr.includes(x);
使い方はこちら:
const includes = <A extends readonly unknown[]>(arr: A, x: unknown): x is A[number] => arr.includes(x); const modes = ['dev', 'stg', 'prd'] as const; const mode: string = process.env.MODE ?? '(undefined)'; if (!includes(modes, mode)) { throw new Error(`unknown mode: ${mode}`); } // ここで mode は 'dev' | 'stg' | 'prd' と推論される switch (mode) { case 'dev': // do something; break; case 'stg': // do something; break; case 'prd': // do something; break; default: // should not get here mode satisfies never; }
TypeScript: TS Playground - An online editor for exploring TypeScript and JavaScript