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