🌀
never型を返す関数で行う型の絞り込みに失敗するパターンがある
概要
Next.js の App Router に notFound
関数というのがあります。
この関数を呼び出すことで即座に処理を中断し、404ページを表示させることができます。
notFound
関数は例外をスローすることで処理を終了させるので(戻り値が never
型の関数なので)、型の絞り込みに利用することもできます。
/app/users/[id]/page.tsx
export default async function Page({ params }) {
const user = await fetchUserById(params.id); // undefined or User
if (!user) notFound(); // user が undefined の場合はここで処理が終了
// 以降 user は User 型として扱えるようになる
return <UserProfile profile={user.profile} />;
}
ここで notFound
関数を拡張した myNotFound
関数を作成し、利用するとします。すると、user
変数の型の絞り込みが正常に行われず型エラーが発生するようになってしまいました。
/app/users/[id]/page.tsx
+const myNotFound = (): never => {
+ // 何らかの追加処理
+ notFound();
+};
export default async function Page({ params }) {
const user = await fetchUserById(params.id); // undefined or User
- if (!user) notFound(); // user が undefined の場合はここで処理が終了
+ if (!user) myNotFound(); // user が undefined の場合はここで処理が終了
return <UserProfile profile={user.profile} />; // error TS18048: 'user' is possibly 'undefined'.
}
notFound
と myNotFound
関数はどちらも戻り値が never
型なので違いはないようにみえるのですが myNotFound
関数は型の絞り込みに失敗してしまいます。
これはなぜでしょうか?
詳細
これはパフォーマンス上の理由で TypeScript の if
文などの条件分岐を解析して型の推測を行う制御フロー分析(Control Flow Analysis: CFA)が明示的な変数の型注釈を必要とするからです。[1]
つまり先ほどの例だと、制御フロー分析では myNotFound
変数が never
型を返す関数だと認識されず、型の絞り込みに失敗してしまったのです。左辺の myNotFound
変数自体に型注釈を入れることで型の絞り込みが正常に行われるようになります。
/app/users/[id]/page.tsx
-const myNotFound = (): never => {
+const myNotFound: () => never = () => {
// 何らかの追加処理
notFound();
};
...
どの書き方だと型の絞り込みが正常に行われるのかそうでないのか以下にまとめてみました。
// 型の絞り込みに成功する書き方
const myNotFound: () => never = () => { ... };
const myNotFound: () => never = function () { ... };
function myNotFound(): never { ... }
// 型の絞り込みに失敗する書き方
const myNotFound = notFound;
const myNotFound = (): never => { ... };
const myNotFound = function (): never { ... };
function myNotFound() { notFound() }
今回紹介した例とは異なりますが、TypeScript の Playground に簡単なデモを用意してみました。こちらもご参考ください。
Discussion
TypeScriptの制御フロー分析における型推論の仕組みが、パフォーマンス面での設計によって影響を受けることを具体的に説明している点が勉強になります。ありがとうございます。