Skip to content

Commit

Permalink
Stricter typing for assertLess, assertLessOrEqual, asserGreater, asse…
Browse files Browse the repository at this point in the history
…rtGreaterOrEqual

- disallow undefined actual values because any comparison with undefined returns false
- disallow null expected values
- narrow the returned type to exclude undefined
  • Loading branch information
dandv committed Dec 9, 2024
1 parent 73e6595 commit 79e9937
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 14 deletions.
9 changes: 7 additions & 2 deletions assert/greater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import { AssertionError } from "./assertion_error.ts";
* @param expected The expected value to compare.
* @param msg The optional message to display if the assertion fails.
*/
export function assertGreater<T>(actual: T, expected: T, msg?: string) {
if (actual > expected) return;
export function assertGreater<T>(
actual: Exclude<T, undefined> | null,
expected: NonNullable<T>,
msg?: string,
): asserts actual is Exclude<T, undefined> | null {
// Coerce null to 0 to avoid "Object is possibly null"
if ((actual ?? 0) > expected) return;

const actualString = format(actual);
const expectedString = format(expected);
Expand Down
9 changes: 5 additions & 4 deletions assert/greater_or_equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { AssertionError } from "./assertion_error.ts";
* @param msg The optional message to display if the assertion fails.
*/
export function assertGreaterOrEqual<T>(
actual: T,
expected: T,
actual: Exclude<T, undefined> | null,
expected: NonNullable<T>,
msg?: string,
) {
if (actual >= expected) return;
): asserts actual is Exclude<T, undefined> | null {
// Coerce null to 0 to avoid "Object is possibly null"
if ((actual ?? 0) >= expected) return;

const actualString = format(actual);
const expectedString = format(expected);
Expand Down
30 changes: 30 additions & 0 deletions assert/greater_or_equal_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertGreaterOrEqual, assertThrows } from "./mod.ts";
import { assertType, type IsExact } from "../testing/types.ts";

Deno.test("assertGreaterOrEqual() matches when actual value is greater or equal than expected value", () => {
assertGreaterOrEqual(2, 1);
assertGreaterOrEqual(1n, 1n);
assertGreaterOrEqual(1.1, 1);
assertGreaterOrEqual(null, 0); // coerced to 0
});

Deno.test("assertGreaterOrEqual() throws when actual value is smaller than expected value", () => {
assertThrows(() => assertGreaterOrEqual(1, 2));
assertThrows(() => assertGreaterOrEqual(null, 1));

// Compile-time errors
// assertThrows(() => assertGreater(undefined, 1));
// assertThrows(() => assertGreater(0, null));
});

Deno.test("assertGreaterOrEqual() on strings", () => {
// Strings
assertGreaterOrEqual("", "");
assertThrows(() => assertGreaterOrEqual("", "a"));
assertThrows(() => assertGreaterOrEqual(null, "a"));
});

Deno.test("assertGreater type narrowing", () => {
const n = 0 as number | undefined;
// @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below
assertGreaterOrEqual(n, 0); // `undefined` narrowed out
assertType<IsExact<typeof n, number>>(true);
const s = "" as string | undefined;
// @ts-expect-error -- `undefined` not allowed for s
assertGreaterOrEqual(s, ""); // `undefined` narrowed out
assertType<IsExact<typeof s, string>>(true);
const b = false as boolean | undefined;
// @ts-expect-error -- `undefined` not allowed for b
assertGreaterOrEqual(b, false); // `undefined` narrowed out
assertType<IsExact<typeof b, boolean>>(true);
});
29 changes: 29 additions & 0 deletions assert/greater_test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertGreater, assertThrows } from "./mod.ts";
import { assertType, type IsExact } from "../testing/types.ts";

Deno.test("assertGreaterOrEqual() matches when actual value is greater than expected value", () => {
assertGreater(2, 1);
assertGreater(2n, 1n);
assertGreater(1.1, 1);
assertGreater(null, -1); // coerced to 0
});

Deno.test("assertGreaterOrEqual() throws when actual value is smaller or equal than expected value", () => {
assertThrows(() => assertGreater(1, 2));
assertThrows(() => assertGreater(null, 0));

// Compile-time errors
// assertThrows(() => assertGreater(undefined, 1));
// assertThrows(() => assertGreater(0, null));
});

Deno.test("assertGreater() on strings", () => {
// Strings
assertGreater("b", "a");
assertThrows(() => assertGreater("", "a"));
assertThrows(() => assertGreater(null, "a"));
});

Deno.test("assertGreater type narrowing", () => {
const n = 0 as number | undefined;
// @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below
assertGreater(n, -1); // `undefined` narrowed out
assertType<IsExact<typeof n, number>>(true);
const s = "a" as string | undefined;
// @ts-expect-error -- `undefined` not allowed for s
assertGreater(s, ""); // `undefined` narrowed out
assertType<IsExact<typeof s, string>>(true);
const b = true as boolean | undefined;
// @ts-expect-error -- `undefined` not allowed for b
assertGreater(b, false); // `undefined` narrowed out
assertType<IsExact<typeof b, boolean>>(true);
});
9 changes: 7 additions & 2 deletions assert/less.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ import { AssertionError } from "./assertion_error.ts";
* @param expected The expected value to compare.
* @param msg The optional message to display if the assertion fails.
*/
export function assertLess<T>(actual: T, expected: T, msg?: string) {
if (actual < expected) return;
export function assertLess<T>(
actual: Exclude<T, undefined> | null,
expected: NonNullable<T>,
msg?: string,
): asserts actual is Exclude<T, undefined> | null {
// Coerce null to 0 to avoid "Object is possibly null"
if ((actual ?? 0) < expected) return;

const actualString = format(actual);
const expectedString = format(expected);
Expand Down
9 changes: 5 additions & 4 deletions assert/less_or_equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { AssertionError } from "./assertion_error.ts";
* @param msg The optional message to display if the assertion fails.
*/
export function assertLessOrEqual<T>(
actual: T,
expected: T,
actual: Exclude<T, undefined> | null,
expected: NonNullable<T>,
msg?: string,
) {
if (actual <= expected) return;
): asserts actual is Exclude<T, undefined> | null {
// Coerce null to 0 to avoid "Object is possibly null"
if ((actual ?? 0) <= expected) return;

const actualString = format(actual);
const expectedString = format(expected);
Expand Down
34 changes: 32 additions & 2 deletions assert/less_or_equal_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertLessOrEqual, assertThrows } from "./mod.ts";
import { assertType, type IsExact } from "../testing/types.ts";

Deno.test("assertLessOrEqual", () => {
Deno.test("assertLessOrEqualOrEqual", () => {
// Numbers
assertLessOrEqual(1, 2);
assertLessOrEqual(1n, 1n);
assertLessOrEqual(1n, 2n);
assertLessOrEqual(1, 1.1);
assertLessOrEqual(null, 1); // coerced to 0

// Failures
assertThrows(() => assertLessOrEqual(2, 1));
assertThrows(() => assertLessOrEqual(null, -1));

// Compile-time errors
// assertThrows(() => assertLessOrEqual(undefined, 1));
// assertThrows(() => assertLessOrEqual(0, null));

// Strings
assertLessOrEqual("a", "a");
assertThrows(() => assertLessOrEqual("a", ""));
assertThrows(() => assertLessOrEqual(null, "a"));
});

Deno.test("assertLessOrEqualOrEqual() type narrowing", () => {
const n = 0 as number | undefined;
// @ts-expect-error -- `undefined` not allowed for n
assertLessOrEqual(n, 0); // `undefined` narrowed out
assertType<IsExact<typeof n, number>>(true);
const s = "" as string | undefined;
// @ts-expect-error -- `undefined` not allowed for s
assertLessOrEqual(s, ""); // `undefined` narrowed out
assertType<IsExact<typeof s, string>>(true);
const b = false as boolean | undefined;
// @ts-expect-error -- `undefined` not allowed for b
assertLessOrEqual(b, false); // `undefined` narrowed out
assertType<IsExact<typeof b, boolean>>(true);
});
29 changes: 29 additions & 0 deletions assert/less_test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertLess, assertThrows } from "./mod.ts";
import { assertType, type IsExact } from "../testing/types.ts";

Deno.test("assertLess", () => {
// Numbers
assertLess(1, 2);
assertLess(1n, 2n);
assertLess(1, 1.1);
assertLess(null, 1); // coerced to 0

// Failures
assertThrows(() => assertLess(2, 1));
assertThrows(() => assertLess(null, -1));

// Compile-time errors
// assertThrows(() => assertLess(undefined, 1));
// assertThrows(() => assertLess(-1, null));

// Strings
assertLess("a", "b");
assertThrows(() => assertLess("a", ""));
assertThrows(() => assertLess(null, "a"));
});

Deno.test("assertLess() type narrowing", () => {
const n = 0 as number | undefined;
// @ts-expect-error -- `undefined` not allowed for n; disable to see compile-time error below
assertLess(n, 1); // `undefined` narrowed out
assertType<IsExact<typeof n, number>>(true);
const s = "" as string | undefined;
// @ts-expect-error -- `undefined` not allowed for s
assertLess(s, "a"); // `undefined` narrowed out
assertType<IsExact<typeof s, string>>(true);
const b = false as boolean | undefined;
// @ts-expect-error -- `undefined` not allowed for b
assertLess(b, true); // `undefined` narrowed out
assertType<IsExact<typeof b, boolean>>(true);
});

0 comments on commit 79e9937

Please sign in to comment.