1/**
2 * Type-level tests for object-path system
3 * These tests verify that TypeScript types work correctly at compile time
4 */
5
6import type { AllObjectPaths, GetObjectPath } from "../src/object-path.ts";
7
8// Type testing utilities
9type Expect<T extends true> = T;
10type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1
11 : 2 ? true
12 : false;
13type NotEqual<X, Y> = Equal<X, Y> extends true ? false : true;
14type IsAny<T> = 0 extends 1 & T ? true : false;
15type NotAny<T> = IsAny<T> extends true ? false : true;
16
17// Test interface
18interface TestData {
19 name: string;
20 count: number;
21 active: boolean;
22 settings: {
23 theme: string;
24 notifications: boolean;
25 };
26 items: Array<{ id: number; label: string }>;
27 tags: string[];
28 nested: {
29 deep: {
30 value: boolean;
31 config: {
32 enabled: true;
33 };
34 };
35 };
36}
37
38// ============================================================================
39// AllObjectPaths tests
40// ============================================================================
41
42// Should allow top-level paths
43type TestPath1 = Expect<
44 Equal<["name"], Extract<AllObjectPaths<TestData>, ["name"]>>
45>;
46type TestPath2 = Expect<
47 Equal<["count"], Extract<AllObjectPaths<TestData>, ["count"]>>
48>;
49
50// Should allow nested paths
51type TestPath3 = Expect<
52 Equal<
53 ["settings", "theme"],
54 Extract<AllObjectPaths<TestData>, ["settings", "theme"]>
55 >
56>;
57
58// Should allow deep nested paths
59type TestPath4 = Expect<
60 Equal<
61 ["nested", "deep", "value"],
62 Extract<AllObjectPaths<TestData>, ["nested", "deep", "value"]>
63 >
64>;
65
66// Should allow array index access
67type TestPath5 = Expect<
68 Equal<[number], Extract<AllObjectPaths<string[]>, [number]>>
69>;
70
71// Should allow array element property access
72type TestPath6 = Expect<
73 Equal<
74 ["items", number, "id"],
75 Extract<AllObjectPaths<TestData>, ["items", number, "id"]>
76 >
77>;
78
79// Should allow empty path for nested objects
80type TestPath7 = Expect<
81 Equal<[], Extract<AllObjectPaths<TestData>, []>>
82>;
83
84// ============================================================================
85// GetObjectPath tests
86// ============================================================================
87
88// Top-level property access
89type GetTest1 = Expect<Equal<GetObjectPath<TestData, ["name"]>, string>>;
90type GetTest2 = Expect<Equal<GetObjectPath<TestData, ["count"]>, number>>;
91type GetTest3 = Expect<Equal<GetObjectPath<TestData, ["active"]>, boolean>>;
92
93// Nested property access
94type GetTest4 = Expect<
95 Equal<
96 GetObjectPath<TestData, ["settings", "theme"]>,
97 string
98 >
99>;
100type GetTest5 = Expect<
101 Equal<
102 GetObjectPath<TestData, ["settings", "notifications"]>,
103 boolean
104 >
105>;
106
107// Deep nested access
108type GetTest6 = Expect<
109 Equal<
110 GetObjectPath<TestData, ["nested", "deep", "value"]>,
111 boolean
112 >
113>;
114type GetTest7 = Expect<
115 Equal<
116 GetObjectPath<TestData, ["nested", "deep", "config", "enabled"]>,
117 true
118 >
119>;
120
121// Array access
122type GetTest8 = Expect<
123 Equal<
124 GetObjectPath<TestData, ["tags"]>,
125 string[]
126 >
127>;
128type GetTest9 = Expect<
129 Equal<
130 GetObjectPath<TestData, ["tags", number]>,
131 string
132 >
133>;
134type GetTest10 = Expect<
135 Equal<
136 GetObjectPath<TestData, ["items"]>,
137 Array<{ id: number; label: string }>
138 >
139>;
140type GetTest11 = Expect<
141 Equal<
142 GetObjectPath<TestData, ["items", number]>,
143 { id: number; label: string }
144 >
145>;
146type GetTest12 = Expect<
147 Equal<
148 GetObjectPath<TestData, ["items", number, "id"]>,
149 number
150 >
151>;
152type GetTest13 = Expect<
153 Equal<
154 GetObjectPath<TestData, ["items", number, "label"]>,
155 string
156 >
157>;
158
159// Object access
160type GetTest14 = Expect<
161 Equal<
162 GetObjectPath<TestData, ["settings"]>,
163 { theme: string; notifications: boolean }
164 >
165>;
166
167// Empty path returns the whole object
168type GetTest15 = Expect<Equal<GetObjectPath<TestData, []>, TestData>>;
169
170// ============================================================================
171// Array element type extraction tests
172// ============================================================================
173
174type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;
175
176type ArrayTest1 = Expect<Equal<ArrayElement<string[]>, string>>;
177type ArrayTest2 = Expect<Equal<ArrayElement<number[]>, number>>;
178type ArrayTest3 = Expect<
179 Equal<ArrayElement<Array<{ id: number }>>, { id: number }>
180>;
181type ArrayTest4 = Expect<
182 Equal<
183 ArrayElement<GetObjectPath<TestData, ["items"]>>,
184 { id: number; label: string }
185 >
186>;
187type ArrayTest5 = Expect<
188 Equal<ArrayElement<GetObjectPath<TestData, ["tags"]>>, string>
189>;
190
191// ============================================================================
192// Conditional type tests for helper functions
193// ============================================================================
194
195// Test that we can extract array element types from paths
196type ExtractArrayElement<
197 Data extends object,
198 Path extends AllObjectPaths<Data>,
199> = GetObjectPath<Data, Path> extends readonly (infer T)[] ? T : never;
200
201type ElementTest1 = Expect<
202 Equal<
203 ExtractArrayElement<TestData, ["items"]>,
204 { id: number; label: string }
205 >
206>;
207type ElementTest2 = Expect<
208 Equal<ExtractArrayElement<TestData, ["tags"]>, string>
209>;
210
211// Test that non-array paths return never
212type ElementTest3 = Expect<
213 Equal<ExtractArrayElement<TestData, ["name"]>, never>
214>;
215
216// Test number extraction
217type IsNumber<
218 Data extends object,
219 Path extends AllObjectPaths<Data>,
220> = GetObjectPath<Data, Path> extends number ? true : false;
221
222type NumberTest1 = Expect<Equal<IsNumber<TestData, ["count"]>, true>>;
223type NumberTest2 = Expect<Equal<IsNumber<TestData, ["name"]>, false>>;
224
225// Test boolean extraction
226type IsBoolean<
227 Data extends object,
228 Path extends AllObjectPaths<Data>,
229> = GetObjectPath<Data, Path> extends boolean ? true : false;
230
231type BooleanTest1 = Expect<Equal<IsBoolean<TestData, ["active"]>, true>>;
232type BooleanTest2 = Expect<Equal<IsBoolean<TestData, ["count"]>, false>>;
233
234// Test object extraction
235type IsObject<
236 Data extends object,
237 Path extends AllObjectPaths<Data>,
238> = GetObjectPath<Data, Path> extends object ? true : false;
239
240type ObjectTest1 = Expect<Equal<IsObject<TestData, ["settings"]>, true>>;
241type ObjectTest2 = Expect<Equal<IsObject<TestData, ["items"]>, true>>;
242type ObjectTest3 = Expect<Equal<IsObject<TestData, ["name"]>, false>>;
243
244// ============================================================================
245// Edge cases
246// ============================================================================
247
248// Readonly arrays should work
249interface ReadonlyData {
250 readonly items: readonly { id: number }[];
251}
252
253type ReadonlyTest1 = Expect<
254 Equal<
255 GetObjectPath<ReadonlyData, ["items"]>,
256 readonly { id: number }[]
257 >
258>;
259type ReadonlyTest2 = Expect<
260 Equal<
261 GetObjectPath<ReadonlyData, ["items", number]>,
262 { id: number }
263 >
264>;
265type ReadonlyTest3 = Expect<
266 Equal<
267 GetObjectPath<ReadonlyData, ["items", number, "id"]>,
268 number
269 >
270>;
271
272// Optional properties
273interface OptionalData {
274 required: string;
275 optional?: number;
276 nested?: {
277 value: boolean;
278 };
279}
280
281type OptionalTest1 = Expect<
282 Equal<GetObjectPath<OptionalData, ["required"]>, string>
283>;
284type OptionalTest2 = Expect<
285 Equal<GetObjectPath<OptionalData, ["optional"]>, number | undefined>
286>;
287
288// Union types
289interface UnionData {
290 value: string | number;
291 items: Array<{ type: "a"; a: string } | { type: "b"; b: number }>;
292}
293
294type UnionTest1 = Expect<
295 Equal<GetObjectPath<UnionData, ["value"]>, string | number>
296>;
297
298// ============================================================================
299// Real-world usage simulation
300// ============================================================================
301
302// Simulate the actual helper function signatures
303type ObjSetSignature<
304 Data extends object,
305 Path extends AllObjectPaths<Data>,
306> = (
307 path: Path,
308 value:
309 | Exclude<GetObjectPath<Data, Path>, Function>
310 | ((prev: GetObjectPath<Data, Path>) => GetObjectPath<Data, Path>),
311) => void;
312
313// This should accept string or function
314declare const objSetName: ObjSetSignature<TestData, ["name"]>;
315objSetName(["name"], "test");
316objSetName(["name"], (prev) => prev.toUpperCase());
317
318// This should accept number or function
319declare const objSetCount: ObjSetSignature<TestData, ["count"]>;
320objSetCount(["count"], 42);
321objSetCount(["count"], (n) => n + 1);
322
323// Array push signature
324type ArrayPushSignature<
325 Data extends object,
326 Path extends AllObjectPaths<Data>,
327> = GetObjectPath<Data, Path> extends readonly (infer T)[] ? (path: Path, ...items: T[]) => void
328 : never;
329
330// This should accept individual items, not arrays
331declare const arrayPushItems: ArrayPushSignature<TestData, ["items"]>;
332arrayPushItems(
333 ["items"],
334 { id: 1, label: "first" },
335 { id: 2, label: "second" },
336);
337
338declare const arrayPushTags: ArrayPushSignature<TestData, ["tags"]>;
339arrayPushTags(["tags"], "alpha", "beta", "gamma");
340
341// Array remove signature
342type ArrayRemoveSignature<
343 Data extends object,
344 Path extends AllObjectPaths<Data>,
345> = GetObjectPath<Data, Path> extends readonly (infer T)[]
346 ? (path: Path, filter: (item: T, index: number) => boolean) => void
347 : never;
348
349declare const arrayRemoveItems: ArrayRemoveSignature<TestData, ["items"]>;
350arrayRemoveItems(["items"], (item) => item.id === 1);
351arrayRemoveItems(["items"], (item, index) => index === 0);
352
353declare const arrayRemoveTags: ArrayRemoveSignature<TestData, ["tags"]>;
354arrayRemoveTags(["tags"], (tag) => tag === "alpha");
355
356// Increment signature
357type IncrementSignature<
358 Data extends object,
359 Path extends AllObjectPaths<Data>,
360> = GetObjectPath<Data, Path> extends number ? (path: Path, amount?: number) => void
361 : never;
362
363declare const increment: IncrementSignature<TestData, ["count"]>;
364increment(["count"]);
365increment(["count"], 5);
366
367// Should not work on non-numbers (type should be never)
368type IncrementNameTest = Expect<
369 Equal<IncrementSignature<TestData, ["name"]>, never>
370>;
371
372// Toggle signature
373type ToggleSignature<
374 Data extends object,
375 Path extends AllObjectPaths<Data>,
376> = GetObjectPath<Data, Path> extends boolean ? (path: Path) => void
377 : never;
378
379declare const toggle: ToggleSignature<TestData, ["active"]>;
380toggle(["active"]);
381
382// Should not work on non-booleans (type should be never)
383type ToggleCountTest = Expect<
384 Equal<ToggleSignature<TestData, ["count"]>, never>
385>;
386
387// ============================================================================
388// Verify no `any` types leaked through
389// ============================================================================
390
391type NoAnyTest1 = Expect<NotAny<GetObjectPath<TestData, ["name"]>>>;
392type NoAnyTest2 = Expect<NotAny<GetObjectPath<TestData, ["items"]>>>;
393type NoAnyTest3 = Expect<NotAny<GetObjectPath<TestData, ["items", number]>>>;
394type NoAnyTest4 = Expect<
395 NotAny<GetObjectPath<TestData, ["settings", "theme"]>>
396>;
397
398export type { ArrayPushSignature, ArrayRemoveSignature, IncrementSignature, ObjSetSignature, ToggleSignature };