From de9f8d2db15e6773609d51b694ed60605fc3cdf4 Mon Sep 17 00:00:00 2001 From: Yusuke Tanaka Date: Sat, 17 Aug 2024 00:10:23 +0900 Subject: [PATCH] feat(assert/unstable): add `assertNever` (#5690) This commit adds a new function `assertNever` to the `assert` submodule. This function is particularly useful when we want to check whether we properly handle all the variants of a discriminated union. Ref: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-exhaustiveness-checking --------- Co-authored-by: Yoshiya Hinosawa --- assert/deno.json | 1 + assert/mod.ts | 1 + assert/never.ts | 103 +++++++++++++++++++++++++++++++++++++++++++ assert/never_test.ts | 36 +++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 assert/never.ts create mode 100644 assert/never_test.ts diff --git a/assert/deno.json b/assert/deno.json index 3e910774f..8397ea5ab 100644 --- a/assert/deno.json +++ b/assert/deno.json @@ -16,6 +16,7 @@ "./less": "./less.ts", "./less-or-equal": "./less_or_equal.ts", "./match": "./match.ts", + "./never": "./never.ts", "./not-equals": "./not_equals.ts", "./not-instance-of": "./not_instance_of.ts", "./not-match": "./not_match.ts", diff --git a/assert/mod.ts b/assert/mod.ts index a3ffd8cd5..ef4fa5b10 100644 --- a/assert/mod.ts +++ b/assert/mod.ts @@ -30,6 +30,7 @@ export * from "./is_error.ts"; export * from "./less_or_equal.ts"; export * from "./less.ts"; export * from "./match.ts"; +export * from "./never.ts"; export * from "./not_equals.ts"; export * from "./not_instance_of.ts"; export * from "./not_match.ts"; diff --git a/assert/never.ts b/assert/never.ts new file mode 100644 index 000000000..da4035783 --- /dev/null +++ b/assert/never.ts @@ -0,0 +1,103 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// This module is browser compatible. +import { format } from "@std/internal/format"; +import { AssertionError } from "./assertion_error.ts"; + +/*! + * Ported and modified from: https://github.com/microsoft/TypeScript-Website/blob/v2/packages/documentation/copy/en/handbook-v1/Unions%20and%20Intersections.md#union-exhaustiveness-checking + * licensed as: + * + * The MIT License (MIT) + * Copyright (c) Microsoft Corporation + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * **UNSTABLE**: New API, yet to be vetted. + * + * Make an assertion that `x` is of type `never`. + * If not then throw. + * + * @experimental + * + * @example Exhaustivenss check + * ```ts + * import { assertNever } from "@std/assert/never"; + * + * type Kinds = "A" | "B"; + * + * function handleKind(kind: Kinds) { + * switch (kind) { + * case "A": + * doA(); + * break; + * case "B": + * doB(); + * break; + * default: + * assertNever(kind); + * } + * } + * + * function doA() { + * // ... + * } + * + * function doB() { + * // ... + * } + * ``` + * + * @example Compile-time error when there is a missing case + * ```ts expect-error ignore + * import { assertNever } from "@std/assert/never"; + * + * type Kinds = "A" | "B" | "C"; + * + * function handleKind(kind: Kinds) { + * switch (kind) { + * case "A": + * doA(); + * break; + * case "B": + * doB(); + * break; + * default: + * // Type error since "C" is not handled + * assertNever(kind); + * } + * } + * + * function doA() { + * // ... + * } + * + * function doB() { + * // ... + * } + * ``` + * + * @param x The value to be checked as never + * @param msg The optional message to display if the assertion fails. + * @returns Never returns, always throws. + * @throws {AssertionError} + */ +export function assertNever(x: never, msg?: string): never { + throw new AssertionError( + msg ?? `Expect ${format(x)} to be of type never`, + ); +} diff --git a/assert/never_test.ts b/assert/never_test.ts new file mode 100644 index 000000000..3df57dbb0 --- /dev/null +++ b/assert/never_test.ts @@ -0,0 +1,36 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +import { AssertionError, assertNever, assertThrows } from "./mod.ts"; + +Deno.test("assertNever: exhaustiveness check", () => { + type Kinds = "A" | "B"; + + function doA() { + // ... + } + + function doB() { + // ... + } + + function handleKind(kind: Kinds) { + switch (kind) { + case "A": + doA(); + break; + case "B": + doB(); + break; + default: + assertNever(kind); + } + } + + handleKind("A"); + handleKind("B"); +}); + +Deno.test("assertNever throws AssertionError", () => { + assertThrows(() => { + assertNever(42 as never); + }, AssertionError); +});