assertie 0.3.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ import { inspect } from "node:util";
2
+ import type { InspectOptions } from "node:util";
3
+ import { AssertieError } from "../../src/index";
4
+
5
+ // For grouping and executing a test case
6
+ type GroupingClosure = () => void;
7
+
8
+ type TestCase = {
9
+ name: string;
10
+ groups: string[];
11
+ closure: GroupingClosure;
12
+ };
13
+
14
+ const GROUP_STACK: string[] = [];
15
+ const TESTS: TestCase[] = [];
16
+ const INSPECT_OPTIONS: InspectOptions = { depth: 4, colors: false };
17
+
18
+ class TestFailure extends Error {
19
+ constructor(message: string) {
20
+ super(message);
21
+ this.name = TestFailure.name;
22
+ }
23
+ }
24
+
25
+ function fail(message: string): never {
26
+ throw new TestFailure(message);
27
+ }
28
+
29
+ function limitStack(stackStr: string): string {
30
+ const lines = stackStr.split("\n");
31
+ for (let i = 0; i < lines.length; i++) {
32
+ // lines below this are noise from the POV of figuring out the test failure
33
+ if (lines[i].includes("Object.closure")) {
34
+ return lines.slice(0, i+1).join("\n");
35
+ }
36
+ }
37
+ return stackStr;
38
+ }
39
+
40
+ /**
41
+ * Groups related runtime tests under a shared name.
42
+ * @param {string} name - The name of the group.
43
+ * @param {GroupingClosure} closure - The closure that registers nested groups and tests.
44
+ */
45
+ export function group(name: string, closure: GroupingClosure): void {
46
+ if (name.includes("SKIP")) return;
47
+ GROUP_STACK.push(name);
48
+ try {
49
+ closure();
50
+ } finally {
51
+ GROUP_STACK.pop();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Registers a runtime test case.
57
+ * @param {string} name - The name of the test.
58
+ * @param {GroupingClosure} closure - The closure that executes the test assertions.
59
+ */
60
+ export function test(name: string, closure: GroupingClosure): void {
61
+ if (name.includes("SKIP")) return;
62
+ TESTS.push({
63
+ name,
64
+ groups: [...GROUP_STACK],
65
+ closure,
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Asserts that the provided closure does not throw.
71
+ * @param {TestFn} fn - The closure that should complete without throwing.
72
+ * @throws {TestFailure} if the closure throws.
73
+ */
74
+ export function mustNotThrow(fn: () => unknown): void {
75
+ try {
76
+ fn();
77
+ } catch (error: unknown) {
78
+ fail(`Expected no throw, got: ${inspect(error, INSPECT_OPTIONS)}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Asserts that the provided closure throws an AssertieError.
84
+ * @param {TestFn} fn - The closure that should throw.
85
+ * @param {RegExp} matcher - Optional regex that must match the thrown error message.
86
+ * @throws {TestFailure} if no error is thrown, a non-AssertieError is thrown, or the message does not match.
87
+ */
88
+ export function mustThrow(fn: () => unknown, matcher?: RegExp): void {
89
+ try {
90
+ fn();
91
+ } catch (error: unknown) {
92
+ if (!(error instanceof AssertieError)) {
93
+ fail(`Expected thrown value to be ${AssertieError.name}, got: ${inspect(error, INSPECT_OPTIONS)}`);
94
+ }
95
+ if (matcher !== undefined && !matcher.test(error.message)) {
96
+ fail(`Expected error message to match ${matcher}, got: "${error.message}"`);
97
+ }
98
+ return;
99
+ }
100
+
101
+ fail("Expected throw, but function completed successfully");
102
+ }
103
+
104
+ /**
105
+ * Asserts that two strings are strictly equal.
106
+ * @param {string} actual - The actual string value.
107
+ * @param {string} expected - The expected string value.
108
+ * @throws {TestFailure} if the values are not equal.
109
+ */
110
+ export function mustEqual(actual: string, expected: string): void {
111
+ if (actual !== expected) {
112
+ fail(`Expected "${expected}", got "${actual}"`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Asserts that the provided boolean is true.
118
+ * @param {boolean} value - The boolean value to assert.
119
+ * @throws {TestFailure} if the value is false.
120
+ */
121
+ export function mustBeTrue(value: boolean): void {
122
+ if (!value) {
123
+ fail("Expected true, got false");
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Asserts that the provided boolean is false.
129
+ * @param {boolean} value - The boolean value to assert.
130
+ * @throws {TestFailure} if the value is true.
131
+ */
132
+ export function mustBeFalse(value: boolean): void {
133
+ if (value) {
134
+ fail("Expected false, got true");
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Executes all registered runtime tests and prints pass/fail output.
140
+ */
141
+ export function executeTests(): void {
142
+ let passed = 0;
143
+ let failed = 0;
144
+
145
+ for (const testCase of TESTS) {
146
+ const name = [...testCase.groups, testCase.name].join(" > ");
147
+ try {
148
+ testCase.closure();
149
+ ++passed;
150
+ console.log(`PASS ${name}`);
151
+ } catch (error: unknown) {
152
+ ++failed;
153
+ console.error(`\nFAIL ${name}`);
154
+ if (error instanceof Error && error.stack !== undefined) {
155
+ console.error(` ${limitStack(error.stack)}\n`);
156
+ } else {
157
+ console.error(` ${inspect(error, INSPECT_OPTIONS)}\n`);
158
+ }
159
+ }
160
+ }
161
+
162
+ const total = passed + failed;
163
+ console.log(`\nRuntime tests finished: ${passed}/${total} passed, ${failed} failed\n`);
164
+
165
+ if (failed > 0) {
166
+ process.exitCode = 1;
167
+ }
168
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "strict": true,
6
+ "moduleResolution": "node",
7
+ "noEmit": true,
8
+ "types": ["node", "vite/client"],
9
+ "skipLibCheck": true
10
+ },
11
+ "include": [
12
+ "../../src/index.ts",
13
+ "./testing.ts",
14
+ "./asserts.test.ts",
15
+ "./assert-helpers.test.ts",
16
+ "./main.ts"
17
+ ]
18
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ assertArrayType,
3
+ assertTupleTypes,
4
+ assertArrayNonNullable,
5
+ assertTupleNonNullable,
6
+ assertPropsNonNullable,
7
+ assertIsTuple,
8
+ assertTypeOfObject,
9
+ assertNonNullable,
10
+ assertTypeOfFunction,
11
+ assertType,
12
+ } from "../../src/index";
13
+
14
+ /* ==================== assertType ==================== */
15
+
16
+ { // Preserves function signature readonly obj parameters on union after narrowing
17
+ function fn(obj: { readonly a: string }): { readonly b: string, c: string } {
18
+ return { b: obj.a, c: obj.a };
19
+ }
20
+ type Fn = typeof fn;
21
+ const union: Fn | string = fn as Fn | string;
22
+ assertType(union, "function");
23
+ const rdonly: { readonly a: string } = { a: "test" };
24
+ const res = union(rdonly);
25
+ // @ts-expect-error
26
+ res.b = "new";
27
+ res.c = "new";
28
+ }
29
+
30
+ /* ==================== assertTypeOfFunction ==================== */
31
+
32
+ { // Preserves readonly obj parameters on union after narrowing
33
+ function fn(obj: { readonly a: string }): { readonly b: string, c: string } {
34
+ return { b: obj.a, c: obj.a };
35
+ }
36
+ type Fn = typeof fn;
37
+ const union: Fn | string = fn as Fn | string;
38
+ assertTypeOfFunction(union);
39
+ const rdonly: { readonly a: string } = { a: "test" };
40
+ const res = union(rdonly);
41
+ // @ts-expect-error
42
+ res.b = "new";
43
+ res.c = "new";
44
+ }
@@ -0,0 +1,238 @@
1
+ import {
2
+ assertArrayType,
3
+ assertTupleTypes,
4
+ assertArrayNonNullable,
5
+ assertTupleNonNullable,
6
+ assertPropsNonNullable,
7
+ assertIsTuple,
8
+ assertTypeOfObject,
9
+ assertNonNullable,
10
+ assertTypeOfFunction,
11
+ assertType,
12
+ } from "../../src/index";
13
+
14
+ /* ==================== assertType ==================== */
15
+
16
+ { // Preserves function signature readonly array on union after narrowing
17
+ function fn(numbers: readonly number[]): readonly number[] {
18
+ return [...numbers];
19
+ }
20
+ type Fn = typeof fn;
21
+ const union: Fn | string = fn as Fn | string;
22
+ assertType(union, "function");
23
+ const rdonly: readonly number[] = [1, 2];
24
+ const res = union(rdonly);
25
+ // @ts-expect-error
26
+ res[0] = 3;
27
+ }
28
+ { // Preserves object readonly properties on union after narrowing
29
+ let obj: { readonly a: string } | string = { a: "test" } as { readonly a: string } | string;
30
+ assertType(obj, "object");
31
+ // @ts-expect-error
32
+ obj.a = "new";
33
+ }
34
+ { // Preserves readonly array after narrowing
35
+ const arr: readonly number[] | string = [1, 2] as readonly number[] | string;
36
+ assertType(arr, "object");
37
+ // @ts-expect-error
38
+ arr.push(3);
39
+ // @ts-expect-error
40
+ arr[0] = 4;
41
+ }
42
+
43
+ /* ==================== assertArrayType ==================== */
44
+
45
+ { // Preserves readonly when narrowing elements
46
+ const arr: readonly (string | number)[] = ["a", "b"] as readonly (string | number)[];
47
+ // @ts-expect-error
48
+ let _string: string = arr[0];
49
+ assertArrayType(arr, "string");
50
+ // read access ok after narrowing
51
+ _string = arr[0];
52
+ // @ts-expect-error
53
+ arr.push("c");
54
+ // @ts-expect-error
55
+ arr[0] = "d";
56
+ }
57
+ { // Preserves mutability when narrowing elements
58
+ const arr: (string | number)[] = ["a", "b"] as (string | number)[];
59
+ assertArrayType(arr, "string");
60
+ arr.push("c");
61
+ arr[0] = "d";
62
+ // @ts-expect-error
63
+ arr.push(1);
64
+ }
65
+ { // Narrows readonly unknown[] to readonly T[]
66
+ const arr: readonly unknown[] = ["a", "b"] as readonly unknown[];
67
+ // @ts-expect-error
68
+ let _string: string = arr[0];
69
+ assertArrayType(arr, "string");
70
+ // read access ok after narrowing
71
+ _string = arr[0];
72
+ // @ts-expect-error
73
+ arr.push("c");
74
+ // @ts-expect-error
75
+ arr[0] = "d";
76
+ }
77
+ { // Narrows mutable unknown[] to mutable T[]
78
+ const arr: unknown[] = ["a", "b"] as unknown[];
79
+ assertArrayType(arr, "string");
80
+ arr.push("c");
81
+ arr[0] = "d";
82
+ }
83
+
84
+ /* ==================== assertTupleTypes ==================== */
85
+
86
+ { // Preserves readonly when narrowing tuple elements
87
+ const tuple: readonly [string | number] = ["a"] as readonly [string | number];
88
+ // @ts-expect-error
89
+ let _string: string = tuple[0];
90
+ assertTupleTypes(tuple, ["string"]);
91
+ // read access ok after narrowing
92
+ _string = tuple[0];
93
+ // @ts-expect-error
94
+ tuple[0] = "b";
95
+ }
96
+ { // Preserves mutability when narrowing tuple elements
97
+ const tuple: [string | number] = ["a"] as [string | number];
98
+ assertTupleTypes(tuple, ["string"]);
99
+ tuple[0] = "b";
100
+ // @ts-expect-error
101
+ tuple[0] = 1;
102
+ }
103
+ { // Narrows readonly tuple of unknowns
104
+ const tuple: readonly [unknown, unknown] = ["a", 1] as readonly [unknown, unknown];
105
+ // @ts-expect-error
106
+ let number: number = tuple[1];
107
+ assertTupleTypes(tuple, ["string", "number"]);
108
+ // read access ok after narrowing
109
+ number = tuple[1];
110
+ // @ts-expect-error
111
+ tuple[1] = 2;
112
+ }
113
+ { // Narrows mutable tuple of unknowns
114
+ const tuple: [unknown, unknown] = ["a", 1] as [unknown, unknown];
115
+ assertTupleTypes(tuple, ["string", "number"]);
116
+ tuple[0] = "b";
117
+ tuple[1] = 2;
118
+ }
119
+
120
+ /* ==================== assertTypeOfFunction ==================== */
121
+
122
+ { // Preserves readonly array on union after narrowing
123
+ function fn(numbers: readonly number[]): readonly number[] {
124
+ return [...numbers];
125
+ }
126
+ type Fn = typeof fn;
127
+ const union: Fn | string = fn as Fn | string;
128
+ assertTypeOfFunction(union);
129
+ const rdonly: readonly number[] = [1, 2];
130
+ const res = union(rdonly);
131
+ // @ts-expect-error
132
+ res[0] = 3;
133
+ }
134
+
135
+ /* ==================== assertArrayNonNullable ==================== */
136
+
137
+ { // Preserves readonly when removing nulls
138
+ const arr: readonly (string | null)[] = ["a"] as readonly (string | null)[];
139
+ // @ts-expect-error
140
+ let _string: string = arr[0];
141
+ assertArrayNonNullable(arr);
142
+ // read access ok after narrowing
143
+ _string = arr[0];
144
+ // @ts-expect-error
145
+ arr.push("b");
146
+ // @ts-expect-error
147
+ arr[0] = "c";
148
+ }
149
+ { // Preserves mutability when removing nulls
150
+ const arr: (string | null)[] = ["a"] as (string | null)[];
151
+ assertArrayNonNullable(arr);
152
+ arr.push("b");
153
+ arr[0] = "c";
154
+ }
155
+
156
+ /* ==================== assertTupleNonNullable ==================== */
157
+
158
+ { // Preserves readonly when removing nulls from tuple
159
+ const tuple: readonly [string | null, number | undefined] = ["a", 1] as readonly [string | null, number | undefined];
160
+ // @ts-expect-error
161
+ let _string: string = tuple[0];
162
+ assertTupleNonNullable(tuple);
163
+ // read access ok after narrowing
164
+ _string = tuple[0];
165
+ // @ts-expect-error
166
+ tuple[0] = "b";
167
+ }
168
+ { // Preserves mutability when removing nulls from tuple
169
+ const tuple: [string | null, number | undefined] = ["a", 1] as [string | null, number | undefined];
170
+ assertTupleNonNullable(tuple);
171
+ tuple[0] = "b";
172
+ tuple[1] = 2;
173
+ }
174
+
175
+ /* ==================== assertPropsNonNullable ==================== */
176
+
177
+ { // Preserves readonly on properties
178
+ const obj: { readonly a?: string; b: number | null } = { a: "test", b: 1 } as { readonly a?: string; b: number | null};
179
+ // @ts-expect-error
180
+ let _string: string = obj.a;
181
+ assertPropsNonNullable(obj, ["a", "b"]);
182
+ // read access ok after narrowing
183
+ _string = obj.a;
184
+ // @ts-expect-error
185
+ obj.a = "new";
186
+ // other property not readonly
187
+ obj.b = 2;
188
+ }
189
+
190
+ /* ==================== assertIsTuple ==================== */
191
+
192
+ { // Preserves readonly on array narrowing to tuple
193
+ const arr: readonly number[] = [1, 2] as readonly number[];
194
+ assertIsTuple(arr, 2);
195
+ // read access ok after narrowing
196
+ arr[0];
197
+ // @ts-expect-error
198
+ arr[2]; // ensure it did actually narrow
199
+ // @ts-expect-error
200
+ arr[0] = 3; // ensure it's still readonly
201
+ }
202
+ { // Preserves mutability on array narrowing to tuple
203
+ const arr: number[] = [1, 2] as number[];
204
+ assertIsTuple(arr, 2);
205
+ // @ts-expect-error
206
+ arr[2]; // ensure it did actually narrow
207
+ arr[0] = 3; // ensure it's mutable
208
+ }
209
+
210
+ /* ==================== assertTypeOfObject ==================== */
211
+
212
+ { // Preserves readonly properties in union narrowing
213
+ const obj: { readonly x: string; y: number } | string = { x: "a", y: 1 } as { readonly x: string; y: number } | string;
214
+ assertTypeOfObject(obj);
215
+ // @ts-expect-error
216
+ obj.x = "b";
217
+ obj.y = 2;
218
+ }
219
+
220
+ /* ==================== assertNonNullable ==================== */
221
+
222
+ { // Preserves readonly on array union
223
+ const arr: readonly number[] | null = [1, 2] as readonly number[] | null;
224
+ // @ts-expect-error
225
+ let _number: number = arr[0];
226
+ assertNonNullable(arr);
227
+ _number = arr[0];
228
+ // @ts-expect-error
229
+ arr.push(3);
230
+ // @ts-expect-error
231
+ arr[0] = 4;
232
+ }
233
+ { // Preserves mutability on array union
234
+ const arr: number[] | null = [1, 2] as number[] | null;
235
+ assertNonNullable(arr);
236
+ arr.push(3);
237
+ arr[0] = 4;
238
+ }
@@ -0,0 +1,29 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+
4
+ import typescript from 'typescript';
5
+
6
+ function atLeast(versionMajorMinor: `${number}.${number}`, major: number, minor?: number): boolean {
7
+ const [verMajor, verMinor] = versionMajorMinor.split('.').map(Number);
8
+ return verMajor > major || (verMajor === major && (minor === undefined || verMinor >= minor));
9
+ }
10
+
11
+ const atLeast5 = atLeast(typescript.versionMajorMinor, 5);
12
+ if (!atLeast5) {
13
+ console.log(`TypeScript < 5.0 detected, skipping incompatible type tests.`);
14
+ }
15
+ const tsconfigPath = atLeast5
16
+ ? 'test/types/tsconfig.json'
17
+ : 'test/types/tsconfig.4.x.json';
18
+ console.log(`Running tests based on ${tsconfigPath}`);
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const tscPath = require.resolve('typescript/lib/tsc.js');
22
+ const result = spawnSync(process.execPath, [tscPath, '-p', tsconfigPath], { stdio: 'inherit' });
23
+
24
+ console.log((result.status === 0) ? 'TypeScript tests passed!' : 'TypeScript tests failed!');
25
+ if (result.error) {
26
+ throw result.error;
27
+ }
28
+
29
+ process.exit(result.status ?? 1);
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": [
4
+ "./type-narrowing.5+.test.ts",
5
+ "./readonly.5+.test.ts",
6
+ ],
7
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "strict": true,
6
+ "moduleResolution": "node",
7
+ "noEmit": true,
8
+ "types": ["vite/client"],
9
+ "skipLibCheck": true
10
+ },
11
+ "include": [
12
+ "../../src/index.ts",
13
+ "./type-narrowing.test.ts",
14
+ "./type-narrowing.5+.test.ts",
15
+ "./readonly.test.ts",
16
+ "./readonly.5+.test.ts",
17
+ ],
18
+ }
@@ -0,0 +1,32 @@
1
+ // Tests for improved type narrowing in TypeScript 5.0+ only
2
+ import { assertType, assertTypeOfFunction } from "../../src/index";
3
+
4
+ { // assertType narrows union to specific function
5
+ type X = string | ((arg: string) => string);
6
+ const x: X = ((arg: string) => arg) as X;
7
+ // @ts-expect-error
8
+ let _res: string = x("test");
9
+ assertType(x, "function");
10
+ _res = x("test");
11
+
12
+ // @ts-expect-error
13
+ x(123);
14
+ // These don't error on TypeScript < 5.0
15
+ // because it only narrows unions to () => {} or (args: any[]) => {}
16
+ // @ts-expect-error
17
+ const _res2: number = x("test");
18
+ }
19
+
20
+ { // assertTypeOfFunction narrows union to specific function
21
+ type X = string | ((arg: string) => string);
22
+ const x: X = ((arg: string) => arg) as X;
23
+ // @ts-expect-error
24
+ let _res: string = x("test");
25
+ assertTypeOfFunction(x);
26
+ _res = x("test");
27
+
28
+ // @ts-expect-error
29
+ x(123);
30
+ // @ts-expect-error
31
+ const _res2: number = x("test");
32
+ }