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.
- package/CHANGELOG.md +24 -1
- package/MIGRATION-GUIDE.md +13 -0
- package/README.md +18 -6
- package/lib/assert-helpers.d.ts +20 -0
- package/lib/assert-helpers.js +66 -0
- package/lib/index.d.ts +49 -56
- package/lib/index.js +56 -117
- package/lib/types.d.ts +29 -0
- package/lib/types.js +1 -0
- package/package.json +9 -4
- package/src/assert-helpers.ts +63 -0
- package/src/index.ts +146 -143
- package/src/types.ts +39 -0
- package/test/runtime/assert-helpers.test.ts +583 -0
- package/test/runtime/asserts.test.ts +641 -0
- package/test/runtime/main.ts +6 -0
- package/test/runtime/testing.ts +168 -0
- package/test/runtime/tsconfig.json +18 -0
- package/test/types/readonly.5+.test.ts +44 -0
- package/test/types/readonly.test.ts +238 -0
- package/test/types/run.ts +29 -0
- package/test/types/tsconfig.4.x.json +7 -0
- package/test/types/tsconfig.json +18 -0
- package/test/types/type-narrowing.5+.test.ts +32 -0
- package/test/types/type-narrowing.test.ts +654 -0
- package/tsconfig.json +1 -0
|
@@ -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,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
|
+
}
|