-
-
Notifications
You must be signed in to change notification settings - Fork 218
Description
Clear and concise description of the problem
I think expect.unreachable is very helpful. I wish this skill set include description about this API.
One use case I found is introducing flow-sensitive typing when I'm inspecting into a value with Discriminated Union Type.
Suggested solution
If you like it, expect.unreachable to be included in description, maybe https://github.com/antfu/skills/blob/main/skills/vitest/references/core-expect.md
Alternative
No response
Additional context
import { UserSchema } from "./user";
import * as v from "valibot";
it("should successfully parse a valid user", () => {
const user = { name: "John Doe", age: 25 };
const result = v.safeParse(UserSchema, user);
if (!result.success) {
expect.unreachable("Should not fail safeParse");
}
// ✅️ output's type is narrowed as I expect
expect(result.output.age).toEqual(25);
});Sorry for Japanese text only, but i have written about it:
https://zenn.dev/yumemi_inc/articles/vitest-expect-unreachable
Markdown version collapsed, for better translation experience (Japanese / English translation in browser is very terrible)
expect.unreachable、かなり便利Zod や Valibot などで作成した schema について、Vitest で Unit テストを記述する際に、「成功したかどうか」の分岐がでた時に少し困りませんか?
import * as v from "valibot"; export const UserSchema = v.object({ name: v.string(), age: v.number(), });// 🔴 うまくいかない例 import { UserSchema } from "./user"; import * as v from "valibot"; it("should successfully parse a valid user", () => { const user = { name: "John Doe", age: 25 }; const result = v.safeParse(UserSchema, user); expect(result.success).toBe(true); // expect だけでは型が絞り込まれず、その場合 output が unknown 型になるので // @ts-expect-error expect(result.output.age).toEqual(25); // ^^^ 'age' プロパティは存在しません。 });
expectだけでは型が絞り込まれないため、型チェックで偽陽性(実行時には正しく動くはずなのに、型チェック時にはエラー)になってしまいます。
result.outputにはアクセスできるのですが、型がunknownのままでUserSchemaの型には絞り込まれていないため、ageプロパティにアクセスできません。:::details 参考:
outputのアクセス自体は失敗せず、unknown型になる理由Valibot の
safeParseの戻り値の型定義を見てみると、以下のようになっています。
- 成功(
success === true)のとき、outputの型が決まる- 失敗(
success === false)のとき、outputの型がunknownになる
- 「
outputプロパティが存在しない」とはならない今回のようなコードを書く時の利便性を考慮してのことですかね…?実際に、
ageのような個別のプロパティではなく、outputの内容全体を比較しさえすれば良いなら、型エラーを起こさずに書けます。また、「✅️完全版コード」のようにして型チェックを有効化したとしても、(
expectの定義がゆるいので)型チェックが特に厳しくなるわけでもありません。なので、実は、
safeParseは、unreachableのメリットの説明のためには少し不完全ですが、論理的に正しい主張のはずなのでそれに免じて許してください。🙇// 実はそんなに問題がない import { UserSchema } from "./user"; import * as v from "valibot"; it("should successfully parse a valid user", () => { const user = { name: "John Doe", age: 25 }; const result = v.safeParse(UserSchema, user); expect(result.success).toBe(true); expect(result.output).toEqual({ name: "John Doe", age: 25 }); // ^^^^^^ unknown 型 });// ✅️ 完全版コード import { UserSchema } from "./user"; import * as v from "valibot"; it("should successfully parse a valid user", () => { const user = { name: "John Doe", age: 25 }; const result = v.safeParse(UserSchema, user); if (!result.success) { expect.unreachable("Should not fail safeParse"); } // 🔴書き間違いだが、toEqual の型定義が緩く、型チェックで検出されない。 expect(result.output).toEqual({ name: "John Doe" }); });:::
そこで使えるのが、
expect.unreachableです。// ✅️ 完全版コード import { UserSchema } from "./user"; import * as v from "valibot"; it("should successfully parse a valid user", () => { const user = { name: "John Doe", age: 25 }; const result = v.safeParse(UserSchema, user); if (!result.success) { expect.unreachable("Should not fail safeParse"); } // ✅️ output の型が、狙い通りに絞り込まれている expect(result.output.age).toEqual(25); });テストをわざと失敗させてみると、以下のようなメッセージを出してくれます。
▼ ドキュメント
https://vitest.dev/api/expect.html#expect-unreachable
assert との違い
実は、この記事を書いたきっかけになったのは、Vitest の
assertを紹介している以下の記事です。
assertもexpect.unreachableと同様に、到達不能コードを TypeScript に教えて型を絞り込むのに使えます。https://zenn.dev/apple_yagi/articles/3fecd12aed68d5
どちらも似たりよったりですが、個人的に
expect.unreachableには「ただのif文で、通常のプロダクションのコードと同様に(型ガードとして有効なガード節として)書けるので、assertの API の知識が不要」という利点があると思います。(もちろん「テストを全体的に
expectを使って書いている場合には」という条件付きですが…)Q.どんなカラクリになってるの? A.
never型による大域脱出の明示
expect.unreachableの型定義を見てみると、以下のようになっています。interface ExpectStatic { /* 中略 */ unreachable: (message?: string) => never;「
neverを返す」という見慣れないシグネチャになっています。このような関数を呼び出すと、「大域脱出」として TypeScript に認識されます。これに条件分岐を組み合わせることで、早期リターンによる型ガードと同様に、型の絞り込みができているんですね。https://typescriptbook.jp/reference/statements/never
同じ仕組みを使った例として: Next.js の
notFound関数 があります。今回と同様、条件分岐の中でnotFoundを呼び出すことで、
- id が undefined のときは Not Found のエラーを見せる
- → そうでない場合の分岐では id が undefined でないことが保証されている
といった型チェックが可能です。
import { notFound } from 'next/navigation' import { fetchUser } from './fetch-user' export default async function Profile({ params }: PageProps<'/users/[id]'>) { const { id } = await params const user = await fetchUser(id) // ^? : User | undefined 型 if (!user) { notFound() } // 以降、user が undefined でないことが保証されている }以上。
Validations
- Follow our Code of Conduct
- Read the Contributing Guide.
- Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
