├── .gitignore ├── .vscode └── settings.json ├── Licence ├── README.md ├── package-lock.json ├── package.json ├── src ├── Audit.ts ├── FindPath.ts ├── FindPaths.ts ├── FindReplace.ts ├── Follow.ts ├── Get.ts ├── Lens.ts ├── Modify.ts ├── Over.ts ├── Replace.ts ├── Search │ ├── ExhaustiveSearch.ts │ ├── Iterators │ │ ├── Array.ts │ │ ├── Fn.ts │ │ ├── Free.ts │ │ ├── Struct.ts │ │ ├── Tuple.ts │ │ ├── index.ts │ │ └── types.ts │ ├── StandardSearch.ts │ └── types.ts ├── index.ts ├── placeholders.ts ├── types.ts └── utils.ts ├── tests ├── FindPaths.ts ├── FindReplace.ts ├── Get.ts ├── Lens.ts ├── Over.ts ├── Readme.ts └── Replace.ts ├── tsconfig-base.json ├── tsconfig-build.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | traceDir 3 | dist 4 | ts-trace -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /Licence: -------------------------------------------------------------------------------- 1 | Copyright 2022 Geoffrey Gourdet 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # type-lenses 2 | 3 | Extract or modify pieces of arbitrarily nested types with type lenses. 4 | 5 | ## How to install 6 | 7 | ``` 8 | npm install type-lenses 9 | ``` 10 | 11 | ## Overview 12 | We are interested in `needle` in the type bellow 13 | ```typescript 14 | type Haystack = Map needle) => void, 'bar'] }>; 15 | ``` 16 | 17 | ### Define a lens 18 | We write a path pointing to our needle: 19 | ```typescript 20 | import { Lens, free, a, r } from 'type-lenses'; 21 | 22 | type FocusNeedle = Lens<[free.Map, 1, 'foo', 0, a, r]>; 23 | ``` 24 | In plain English, the steps are as follows: 25 | 1) Focus on the type `Map` 26 | 1) Focus on the argument at index `1` in the arguments list 27 | 1) Focus on the field `"foo"` in the object 28 | 1) Focus on the element at index `0` in the tuple 29 | 1) Focus on the first parameter of the function 30 | 1) Focus on the return type of the function 31 | 32 | ### Extract a piece of type 33 | We can extract our `needle` with `Get`: 34 | ```typescript 35 | import { Get } from 'type-lenses'; 36 | 37 | type Needle = Get; 38 | ``` 39 | It results in: 40 | ``` 41 | needle 42 | ``` 43 | ### Replace a piece of type 44 | We can replace `needle` by any compatible type with `Replace`: 45 | ```typescript 46 | import { Replace } from 'type-lenses'; 47 | 48 | type YihaStack = Replace; 49 | ``` 50 | ```typescript 51 | // result: 52 | Map 'Yiha!') => void, 'bar'] }> 53 | ``` 54 | ### Map over a type 55 | Similarily to `Replace`, `Over` lets us replace `needle` with the result of applying it to a compatible ready-made or custom free type: 56 | 57 | ```typescript 58 | import { Over } from 'type-lenses'; 59 | 60 | type PromiseStack = Over; 61 | ``` 62 | ```typescript 63 | // result: 64 | Map Promise) => void, 'bar'] }> 65 | ``` 66 | 67 | ### Find and replace 68 | 69 | `FindReplace` removes the need to construct a path, as long as we know what `needle` to target: 70 | 71 | ```typescript 72 | import { FindReplace } from 'type-lenses'; 73 | 74 | type YihaStack = FindReplace; 75 | ``` 76 | ```typescript 77 | // result: 78 | Map 'Yiha!') => void, 'bar'] }> 79 | ``` 80 | 81 | It also accepts a replace callback of type `Type<[Needle, Path?]>` if you need to run arbitrary logic: 82 | 83 | ```typescript 84 | // any unary free type can work 85 | type Foo = FindReplace<{ a: 1, b: 2 }, number, free.Promise>; 86 | 87 | import { $ReplaceCallback } from 'type-lenses'; 88 | import { Optional, Last, A, B } from 'free-types'; 89 | 90 | // or one of your design (don't freak out, see the doc) 91 | interface $Callback extends $ReplaceCallback { 92 | type: this['prop'] extends 'a' ? Add<10, A> 93 | : this['prop'] extends 'b' ? Promise> 94 | : never 95 | prop: Last> 96 | } 97 | 98 | type Bar = FindReplace<{ a: 1, b: 2 }, number, $Callback>; 99 | ``` 100 | ```typescript 101 | // result: 102 | type Foo = { a: Promise<1>, b: Promise<2> } 103 | type Bar = { a: 11, b: Promise<2> } 104 | ``` 105 | 106 | `FindReplace` gives control over the search, the number of matches and the way they are replaced, with some limitations to keep in mind. Make sure to read the documentation. 107 | 108 | ### Find paths 109 | We can find paths with `FindPath` and `FindPaths`. 110 | 111 | The former is guaranteed to return a single path pointing to `needle`, or `never`: 112 | 113 | ```typescript 114 | import { FindPath } from 'type-lenses'; 115 | 116 | type PathToNeedle = FindPath; 117 | ``` 118 | ```typescript 119 | // result: 120 | [free.Map, 1, "foo", 0, Param<0>, Output] 121 | ``` 122 | - `Param<0>` and `Output` are aliases for `a` and `r`; 123 | - `free.Map` is a built-in free type, but you can also register your own so they can be inferred (see [doc/$Type](#type)); 124 | - The behaviour for singling out a match is documented in [doc/FindPath(s)](#findpaths). 125 | 126 | The latter returns a tuple of every path leading to `needle`. If it is `self` (the default), it returns every possible path, which can be useful for exploring a type. 127 | ```typescript 128 | import { FindPaths } from 'type-lenses'; 129 | 130 | type EveryPath = FindPaths; 131 | ``` 132 | ```typescript 133 | // result: 134 | [[free.Map], 135 | [free.Map, 0], 136 | [free.Map, 1], 137 | [free.Map, 1, "foo"], 138 | [free.Map, 1, "foo", 1], 139 | [free.Map, 1, "foo", 0], 140 | [free.Map, 1, "foo", 0, r], 141 | [free.Map, 1, "foo", 0, a], 142 | [free.Map, 1, "foo", 0, a, a], 143 | [free.Map, 1, "foo", 0, a, r]] 144 | ``` 145 | 146 | `FindPath(s)` give control over the search and the number of matches, with some limitations to keep in mind. Make sure to read the documentation. 147 | 148 | ### Audit queries 149 | 150 | Finally, we can type check a query, for example in a function: 151 | 152 | ```typescript 153 | declare const foo: < 154 | const Path extends readonly string[] & Check, 155 | Obj extends object, 156 | Check = Audit 157 | >(path: Path, obj: Obj) => void; 158 | 159 | foo(['q', 'b'], { a: { b: 42, c: 2001 }}) 160 | // ~~~ Type "q" is not assignable to type "a" 161 | ``` 162 | This behaviour also enables reliable auto-completion: 163 | 164 | ```typescript 165 | foo([''], {a: { b: 42, c: 2001 }}) 166 | // -- suggest "a" 167 | 168 | foo(['a', ''], {a: { b: 42, c: 2001 }}) 169 | // -- suggest "b" | "c" 170 | ``` 171 | # Documentation 172 | 173 | [Type Checking](#type-checking) | [Lens](#lens) | [Query](#Query) | [Type](#type) | [Get](#get) | [GetMulti](#getmulti) | [Replace](#replace) | [Over](#over) | [FindReplace](#findreplace) | [FindPath(s)](#findpaths) | [Audit](#audit) | [Free utils](#get-getmulti-replace-over) 174 | 175 | ### Type checking 176 | 177 | The library type checks your inputs in various ways, but these checks never involve the `Haystack`. 178 | 179 | This is because the type checker fails to check generics, even with adequate type constraints. Since working with a generic `Haystack` is a very common use case for lenses, I chose to ignore this check for ease of use. 180 | 181 | If you need to check your query, you can do so with a `Lens`. 182 | 183 | 184 | ### `Lens` 185 | 186 | `Lens` 187 | 188 | You can create a lens by passing a `Query` to `Lens`. 189 | 190 | ```typescript 191 | type A = Lens<1>; 192 | type B = Lens<['a', 2, r]>; 193 | ``` 194 | 195 | Utils such as `Get` or `Replace` promote every `Query` to a `Lens`, but it is advised to work with lenses when you want to reuse or compose paths. 196 | 197 | Composing lenses is as simple as wrapping them in a new `Lens`: 198 | 199 | ```typescript 200 | type C = Lens<[A, B]> // Lens<[1, 'a', 2, r]> 201 | ``` 202 | 203 | `Lens` optionally takes a `Model` against which to perform type checking. 204 | 205 | ```typescript 206 | type Haystack = Map needle) => void, 'bar'] }>; 207 | type FocusNeedle = Lens<[free.Map, 1, 'bar', 0, a, r], Haystack>; 208 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 209 | // Type '[$Map, 1, "bar", 0, a, Output]' does not satisfy the constraint '[$Map, 1, "foo", ...QueryItem[]]'. 210 | // Type at position 2 in source is not compatible with type at position 2 in target. 211 | // Type '"bar"' is not assignable to type '"foo"' 212 | ``` 213 | 214 | ### Query 215 | 216 | A `Query` is a `Lens`, a `Path` or a `PathItem` 217 | 218 | A `Path` is a tuple of `PathItem`. 219 | 220 | Path items can be one of the following types: 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 |
typedescription
LensAs we have already seen, nested lenses are flattened.
Param<⁠index>
or ab, ...f
Focuses on a parameter of a function.
Output or r

Focuses on the return type of a function. 235 | 236 | `r` can be an alias of `Output` but can also optionally take an argument: `r` is equivalent to `Lens<[a, r]>`.

stringFocuses on the field of a given name in an object.
numberFocuses on the item at a given index in a tuple.
selfBy default it refers to the haystack, but some utils enable providing a value for it.
$TypeFocuses on the arguments list of a type for which $Type is a free type constructor.
255 | 256 | ### `$Type` 257 | 258 | In a nutshell: 259 | 260 | ```typescript 261 | // A class we want to reference in a path 262 | class Foo { 263 | constructor(private value: T) {} 264 | } 265 | ``` 266 | ```bash 267 | npm install free-types 268 | ``` 269 | ```typescript 270 | import { Type, A } from 'free-types' 271 | 272 | // A free type constructor for that class 273 | interface $Foo extends Type<[A: number]> { 274 | type: Foo> 275 | } 276 | 277 | // Imagining a haystack where the needle is the first argument of Foo 278 | type Haystack = Foo 279 | 280 | // Our path would look like this 281 | type FocusNeedle = Lens<[$Foo, 0]>; 282 | 283 | type Needle = Get; // needle 284 | ``` 285 | ```typescript 286 | // We can also define a free utility type 287 | interface $Exclaim extends Type<[string]> { type `${A}!` } 288 | 289 | type Exclamation = Over<['a'], { a: 'Hello' }, $Exclaim> // { a: "Hello!" } 290 | ``` 291 | 292 | `type-lenses` re-exports a [dozen](https://github.com/geoffreytools/free-types/blob/public/doc/Documentation.md#free) built-in free type constructors under the namespace `free`. 293 | 294 | If you want `FindPaths` to be able to find your own free types, you must register them: 295 | 296 | ```typescript 297 | declare module 'free-types' { 298 | interface TypesMap { Foo: $Foo } 299 | } 300 | ``` 301 | 302 | See [free-types](https://github.com/geoffreytools/free-types) for more information. 303 | 304 | 305 | ## Querying and modifying types 306 | 307 | ### `Get` 308 | Return the queried piece of type or `never` if it is not found. 309 | 310 | #### Syntax 311 | `Get` 312 | 313 | |parameter| description| 314 | |-|-| 315 | |Query| a `Query` 316 | |Haystack| The type you want to extract a piece of type from 317 | |Self| A type which you want `self` to point to. It is `Haystack` by default 318 | 319 | ### `GetMulti` 320 | The same as `Get`, but takes a tuple of `Query` and returns a tuple of results. 321 | 322 | #### Syntax 323 | `GetMulti` 324 | 325 | ### `Replace` 326 | 327 | Replace the queried piece of type with a new value in the parent type, or return the parent type unchanged if the query is `never` or doesn't match anything. 328 | 329 | #### Syntax 330 | `Replace` 331 | 332 | |parameter| description| 333 | |-|-| 334 | |Query| a `Query` 335 | |Haystack| The type you want to modify 336 | |Value | Any type you want to replace the needle with 337 | |Constraint | A mean of turning off type checking in higher order scenarios 338 | 339 | #### Type checking 340 | `Value` is type checked against the `Query`: 341 | ```typescript 342 | type Failing = Replace<[free.WeakSet, 0], WeakSet<{ foo: number }>, number> 343 | // --------------- ~~~~~~ 344 | type Failing = Replace<[free.WeakSet], WeakSet<{ foo: number }>, [number]> 345 | // ------------ ~~~~~~~~ 346 | // Type 'number' does not satisfy the constraint 'object' 347 | ``` 348 | 349 | If `Query` is generic, you will have to opt-out from type checking by setting `Constraint` to `any`: 350 | 351 | ```typescript 352 | type Generic = Replace, object, any> 353 | // --- 354 | ``` 355 | 356 | 357 | ### `Over` 358 | 359 | Map over the parent type, replacing the queried piece of type with the result of applying it to the provided free type. Return the parent type unchanged if the query failed. 360 | 361 | #### Syntax 362 | `Over` 363 | 364 | |parameter| description| 365 | |-|-| 366 | |Query| a `Query` 367 | |Haystack| The type you want to modify 368 | |$Type | A free type constructor 369 | |Constraint | A mean of turning off type checking in higher order scenarios 370 | 371 | #### Type checking 372 | The return type of `$Type` is fully type checked against the `Query`, however its parameters are only checked loosely for relatedness, because they also depend on the `Haystack` which is purposely excluded from type checking: 373 | ```typescript 374 | type Failing = Over<[free.WeakSet, 0], WeakSet<{foo: number}>, $Next> 375 | // ~~~~~ 376 | // Type '$Next' does not satisfy the constraint 'Type<[Unrelated]>' 377 | 378 | type NotFailing = Over<[free.Set, 0], Set<'hello'>, $Next> 379 | // will blow up with no error ------- ----- 380 | ``` 381 | 382 | If `Query` is generic, you will have to opt-out from type checking by setting `Constraint` to `any`: 383 | 384 | ```typescript 385 | type Generic = Over, $Next, any> 386 | // --- 387 | ``` 388 | ### `FindReplace` 389 | 390 | Find a `Needle` in the parent type and replace matches with new values, or return the parent type unchanged if there is no match. 391 | 392 | The search behaves like [`FindPaths`](#findpaths). 393 | 394 | If there are more matches than you specified replace values, the last replace value is used to replace the supernumerary matches: 395 | 396 | ```typescript 397 | type WithValues = FindReplace<[1, 2, 3], number, [42, 2001]>; 398 | // type WithValues = [42, 2001, 2001] 399 | ``` 400 | 401 | > **Warning** 402 | > Do not expect object properties to be found and replaced in a specific order. If you need to find/replace multiple values in the same object, use a replace callback instead of a tuple of values. 403 | 404 | #### Syntax 405 | `FindReplace` 406 | 407 | |parameter| description| 408 | |-|-| 409 | |Haystack| The type you want to modify 410 | |Needle| The piece of type you want to search 411 | |Values \| $Type| A tuple of values to replace the matches with, or a replace callback 412 | |From| A path from which to start the search. 413 | |Limit| The maximum number of matches to find and replace 414 | 415 | #### Type checking 416 | 417 | If you use a replace callback, its first parameter must extend your `Needle`. 418 | 419 | #### Replace callback 420 | 421 | If you want to define a custom replace callback, you can extend `$ReplaceCallback` which is really `Type<[T, Path?]>` where `T` is your `Needle`: 422 | 423 | ```typescript 424 | import { $ReplaceCallback } from 'type-lenses'; 425 | import { Optional, Last, A, B } from 'free-types'; 426 | 427 | interface $Callback extends $ReplaceCallback { 428 | type: this['prop'] extends 'a' ? Add<10, A> 429 | : this['prop'] extends 'b' ? Promise> 430 | : never 431 | prop: Last> 432 | } 433 | 434 | type WithCallback = FindReplace<{ a: 1, b: 2 }, number, $Callback>; 435 | // type WithCallback = { a: 11, b: Promise<2> } 436 | ``` 437 | 438 | The types `Optional`, `A` and `B` let you safely index `this` to extract the arguments passed to `$Callback`, while defusing type constraints. 439 | 440 | - `Add` expects a `number`, which is satisfied by `A`; 441 | - `Last` expects a tuple, which is satisfied by `Optional`. 442 | 443 | More information about these helpers in free-types' [guide](https://github.com/geoffreytools/free-types/blob/public/doc/Guide.md#helpers). 444 | 445 | Here I also created a `prop` field for clarity, using `Last` to select the last `PathItem` in the `Path`. 446 | 447 | ### `FindPath(s)` 448 | 449 | `FindPath` is literally defined like so: 450 | 451 | ```typescript 452 | type FindPath = 453 | Extract[0], [any, ...any[]]>; 454 | ``` 455 | 456 | 457 | `FindPaths` returns a tuple of every path leading to the `Needle`, or every possible path when `Needle` is `self` (the default). 458 | 459 | The search results are ordered according to the following rules, ranked by precedence: 460 | 461 | 1) Matches closer to the root are listed first (breadth-first search); 462 | 1) Matches honour the ordering of tuples and function arguments lists; 463 | 1) Matches **do not** honour the ordering of object properties; 464 | 1) In function signatures, matched parameters are listed before any matched return type; 465 | 1) the needles `any`, `never` and `unknown` match `any`, `never` and `unknown` respectively (use `self` to match every path); 466 | 1) When the needle is `self`, the ordering of paths which do not lead to a `BaseType` (a leaf) is unspecified. 467 | 468 | ```typescript 469 | type BaseType = string | number | boolean | symbol | undefined | void; 470 | ``` 471 | 472 | #### Syntax 473 | `FindPaths` 474 | 475 | |parameter| description| 476 | |-|-| 477 | |T| The type you want to probe 478 | |Needle| The piece of type you want selected paths to point to. It defaults to `self`, which selects every possible path. 479 | |From| A path from which to start the search. 480 | |Limit| The maximum number of matches to return 481 | 482 | #### From 483 | 484 | `From` enables you to specify which path should be searched for potential matches. It can be used for disambiguation or to improve performance: 485 | 486 | ```typescript 487 | type PathsSubset = FindPaths<{ a: [1], b: [2] }, number, ['b']> 488 | // type PathsSubset = [['b', 0]] 489 | ``` 490 | 491 | #### Limit 492 | 493 | `Limit` enables you to ignore matches which are of no interest to you. It also improves performance: 494 | 495 | ```typescript 496 | // provide an empty `From` to access this parameter vv 497 | type PathsSubset = FindPaths<{ a: [1], b: [2] }, number, [], 1> 498 | // type PathsSubset = [['a', 0]] 499 | ``` 500 | 501 | ### `Audit` 502 | 503 | `Audit` is the type being used internally to type check `Lens`, but it can be used with functions as well. 504 | 505 | It returns a `Suggestion` which is a type related to a `Query` that can contain unions and be open-ended: 506 | 507 | ```typescript 508 | type Suggestion = Audit<['q', 'b'], { a: { b: number, c: number }}>; 509 | // type Suggestion: ['a', ...QueryItem[]] 510 | 511 | type Suggestion = Audit<['a', 'd'], { a: { b: number, c: number }}>; 512 | // type Suggestion: ['a', 'b' | 'c'] 513 | ``` 514 | 515 | Success is represented either by `QueryItem`, `QueryItem[]` or `readonly QueryItem[]` depending on your input. You should not need to check for success, but if you do, consider using the companion type `Successful` which returns a boolean: 516 | 517 | ```typescript 518 | type OK = Successful>; 519 | // type OK: true 520 | ``` 521 | 522 | #### Syntax 523 | `Audit` 524 | 525 | |parameter| description| 526 | |-|-| 527 | |Query| The `Query` you want to type check 528 | |Model| The type that should to be traversable by the `Query` 529 | 530 | #### Generic Query 531 | 532 | Be mindful that type-checking the query will make your function unusable in higher order scenarios. 533 | 534 | ```typescript 535 | declare const foo: < 536 | const Path extends readonly string[] & Check, 537 | Obj extends object, 538 | Check = Audit 539 | >(path: Path, obj: Obj) => void; 540 | 541 | const bar = < 542 | const Path extends readonly string[], 543 | Obj extends object 544 | >(path: Path, obj: Obj) => foo(path, obj) 545 | // cryptic error ~~~~ 546 | ``` 547 | An obvious workaround is to check the input in `bar` and pass the check to `foo` as a type parameter: 548 | ```typescript 549 | const bar = < 550 | const Path extends readonly string[] & Check, 551 | Obj extends object, 552 | Check = Audit 553 | >(path: Path, obj: Obj) => 554 | foo(path, obj) 555 | ``` 556 | 557 | Alternatively, you could make type checking optional: 558 | ```typescript 559 | /** pass `any` to `_` in order to disable type-checking*/ 560 | declare const foo: < 561 | const Path extends readonly string[] & Check, 562 | Obj extends object, 563 | Check = Audit 564 | >(path: Path, obj: Obj, _?: Check) => void; 565 | // --------- 566 | 567 | const bar = (path: readonly string[], obj: object) => 568 | foo(path, { a: { b: null }}, null as any) 569 | // ----------- 570 | ``` 571 | ### `$Get`, `$GetMulti`, `$Replace`, `$Over` 572 | Free versions of `Get`, `GetMulti`, `Replace` and `Over`. 573 | 574 | Can be used like so: 575 | ```typescript 576 | import { apply } from 'free-types'; 577 | 578 | type $NeedleSelector = $Get 579 | type Needle = apply<$NeedleSelector, [Haystack]>; 580 | ``` 581 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-lenses", 3 | "version": "0.9.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "type-lenses", 9 | "version": "0.9.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "free-types": "^0.12.0", 13 | "free-types-core": "^0.8.5" 14 | }, 15 | "devDependencies": { 16 | "ts-spec": "^1.5.0", 17 | "ts-trace": "github:geoffreytools/ts-trace", 18 | "typescript": "^5.1.6" 19 | } 20 | }, 21 | "node_modules/free-types": { 22 | "version": "0.12.0", 23 | "resolved": "https://registry.npmjs.org/free-types/-/free-types-0.12.0.tgz", 24 | "integrity": "sha512-lLTvJNRfqTnqEoNHTnd2gz82yJINVdaX6bmSzMrtHc/+BxxP0CAPwcBgdJw4bKXWNuhCV9JtdCN83VNGUH0qKQ==", 25 | "dependencies": { 26 | "free-types-core": "^0.8.4" 27 | } 28 | }, 29 | "node_modules/free-types-core": { 30 | "version": "0.8.5", 31 | "resolved": "https://registry.npmjs.org/free-types-core/-/free-types-core-0.8.5.tgz", 32 | "integrity": "sha512-RxAcBBmtAIpIRfWsMDKtaBW8cENHbRH3vrXT05IbpmSx6Mrm2FtRA6tHhHgWoXrrFnXe0SzAZOb/WyN2BNIZ3w==" 33 | }, 34 | "node_modules/ts-spec": { 35 | "version": "1.5.0", 36 | "resolved": "https://registry.npmjs.org/ts-spec/-/ts-spec-1.5.0.tgz", 37 | "integrity": "sha512-+rP1pduHEFEt89ur4Ibo+huIuLPD7wZt/4gtp2S1TMsqchyCUvFQhLpEWQTv5qClUcF1X/h5EgMbUR91GzVIPA==", 38 | "dev": true, 39 | "dependencies": { 40 | "free-types-core": "^0.8.0" 41 | } 42 | }, 43 | "node_modules/ts-trace": { 44 | "version": "1.3.1", 45 | "resolved": "git+ssh://git@github.com/geoffreytools/ts-trace.git#9a51b02a24fb9e4d3b445f5480aad9afd889ecca", 46 | "dev": true, 47 | "license": "ISC" 48 | }, 49 | "node_modules/typescript": { 50 | "version": "5.1.6", 51 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 52 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 53 | "dev": true, 54 | "bin": { 55 | "tsc": "bin/tsc", 56 | "tsserver": "bin/tsserver" 57 | }, 58 | "engines": { 59 | "node": ">=14.17" 60 | } 61 | } 62 | }, 63 | "dependencies": { 64 | "free-types": { 65 | "version": "0.12.0", 66 | "resolved": "https://registry.npmjs.org/free-types/-/free-types-0.12.0.tgz", 67 | "integrity": "sha512-lLTvJNRfqTnqEoNHTnd2gz82yJINVdaX6bmSzMrtHc/+BxxP0CAPwcBgdJw4bKXWNuhCV9JtdCN83VNGUH0qKQ==", 68 | "requires": { 69 | "free-types-core": "^0.8.4" 70 | } 71 | }, 72 | "free-types-core": { 73 | "version": "0.8.5", 74 | "resolved": "https://registry.npmjs.org/free-types-core/-/free-types-core-0.8.5.tgz", 75 | "integrity": "sha512-RxAcBBmtAIpIRfWsMDKtaBW8cENHbRH3vrXT05IbpmSx6Mrm2FtRA6tHhHgWoXrrFnXe0SzAZOb/WyN2BNIZ3w==" 76 | }, 77 | "ts-spec": { 78 | "version": "1.5.0", 79 | "resolved": "https://registry.npmjs.org/ts-spec/-/ts-spec-1.5.0.tgz", 80 | "integrity": "sha512-+rP1pduHEFEt89ur4Ibo+huIuLPD7wZt/4gtp2S1TMsqchyCUvFQhLpEWQTv5qClUcF1X/h5EgMbUR91GzVIPA==", 81 | "dev": true, 82 | "requires": { 83 | "free-types-core": "^0.8.0" 84 | } 85 | }, 86 | "ts-trace": { 87 | "version": "git+ssh://git@github.com/geoffreytools/ts-trace.git#9a51b02a24fb9e4d3b445f5480aad9afd889ecca", 88 | "dev": true, 89 | "from": "ts-trace@github:geoffreytools/ts-trace" 90 | }, 91 | "typescript": { 92 | "version": "5.1.6", 93 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", 94 | "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", 95 | "dev": true 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-lenses", 3 | "version": "0.9.1", 4 | "description": "Extract or modify pieces of arbitrarily nested types with type lenses", 5 | "types": "./dist/index.d.ts", 6 | "main": "./dist/index.js", 7 | "author": "Geoffrey Gourdet", 8 | "license": "ISC", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/geoffreytools/type-lenses" 12 | }, 13 | "files": [ 14 | "./dist/**/*" 15 | ], 16 | "devDependencies": { 17 | "ts-spec": "^1.5.0", 18 | "ts-trace": "github:geoffreytools/ts-trace", 19 | "typescript": "^5.1.6" 20 | }, 21 | "dependencies": { 22 | "free-types": "^0.12.0", 23 | "free-types-core": "^0.8.5" 24 | }, 25 | "scripts": { 26 | "build": "tsc -p ./tsconfig-build.json", 27 | "watch": "tsc --watch", 28 | "trace": "bash ./node_modules/ts-trace/ts-trace.sh" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Audit.ts: -------------------------------------------------------------------------------- 1 | import { unwrap, Type, B, Slice } from 'free-types-core'; 2 | import { Fn, Param, Output, Query, ILens, Path, QueryItem } from './types'; 3 | import { Prev, Next, Parameters, ToNumber, GenericFree, SequenceTo, MapOver, IsAny } from './utils'; 4 | import { Lens } from './Lens'; 5 | import { FollowPath, NOT_FOUND } from './Follow'; 6 | 7 | export type Successful = 8 | QueryItem[] extends Check ? true 9 | : QueryItem extends Check ? true 10 | : false 11 | 12 | export type Audit< 13 | Q extends Query, 14 | Model, 15 | I extends number = 0, 16 | L extends ILens = Lens, 17 | F = FollowPath 18 | > = IsAny extends true ? Success 19 | : [F] extends [NOT_FOUND] ? HandleError 20 | : Next extends L['path']['length'] 21 | ? Success 22 | : Audit, L>; 23 | 24 | type Success = Q extends readonly unknown[] 25 | ? Q extends unknown[] ? QueryItem[] : readonly QueryItem[] 26 | : QueryItem 27 | 28 | type HandleError< 29 | Model, 30 | Q extends Query, 31 | L extends ILens, 32 | I extends number, 33 | R extends readonly QueryItem[] = SaveReadonly> 34 | > = Q extends ILens ? Lens 35 | : Q extends [ILens] ? [Lens] 36 | : Q extends readonly QueryItem[] ? MapOver> 37 | : Q extends QueryItem ? R[0] 38 | : R 39 | 40 | type SaveReadonly = 41 | Q extends unknown[] ? P : readonly [...P]; 42 | 43 | interface $WrapIfLens extends Type<2> { 44 | type: Q[B] extends ILens ? Lens : this[0] 45 | constraints: [QueryItem, number] 46 | } 47 | 48 | type ProperPath> = 49 | [N] extends [never] 50 | ? LastPathItem 51 | : [...LastPathItem, ...Rest] 52 | 53 | type Rest = 54 | Next extends L['path']['length'] ? [N] : [N, ...QueryItem[]]; 55 | 56 | type LastPathItem

= 57 | I extends 0 ? [] : Slice; 58 | 59 | type NextPathItem = 60 | readonly any[] extends Model ? number 61 | : Model extends readonly unknown[] ? NumericArgs 62 | : Model extends Fn ? Output | Param['length']>>> 63 | : Model extends GenericFree ? unwrap['type'] 64 | : Model extends Record ? { [K in keyof Model]: K }[keyof Model] 65 | : never 66 | 67 | type NumericArgs = ToNumber -------------------------------------------------------------------------------- /src/FindPath.ts: -------------------------------------------------------------------------------- 1 | import { Path } from "./types"; 2 | import { FindPaths } from "./FindPaths"; 3 | 4 | export type FindPath = 5 | Extract[0], [any, ...any[]]>; -------------------------------------------------------------------------------- /src/FindPaths.ts: -------------------------------------------------------------------------------- 1 | import { GenericFree, IsAny, GetOrElse, IsArray } from "./utils"; 2 | import { Fn, NOT_FOUND, Path, self } from "./types"; 3 | import { Get } from "./Get"; 4 | import { apply, Checked, Lossy, At } from "free-types-core"; 5 | import { $Iterator, $Struct, $Tuple, $Array, $Fn, $Free } from "./Search/Iterators"; 6 | import { $DeepSearch, $SearchMode, IsNeedle, MatchAll } from "./Search/types"; 7 | import * as Standard from "./Search/StandardSearch"; 8 | import * as Exhaustive from "./Search/ExhaustiveSearch"; 9 | 10 | export { FindPaths} 11 | 12 | type FindPaths< 13 | T, 14 | Needle = self, 15 | From extends Path = [], 16 | Limit extends number = number, 17 | $Search extends $SearchMode = SelectSearchMode 18 | > = [T] extends [T] 19 | ? From extends [] 20 | ? GetPaths extends infer P 21 | extends unknown[][] ? FormatSelf

: [] 22 | : GetPaths, Needle, Limit, $Search> extends infer P 23 | extends unknown[][] 24 | ? P extends [] ? From : FormatSelf<{ 25 | [K in keyof P]: [...From, ...P[K]] 26 | }> : [] 27 | : never 28 | 29 | type SelectSearchMode = 30 | MatchAll extends true 31 | ? Exhaustive.SearchMode> 32 | : Standard.SearchMode> 33 | 34 | type GetPaths< 35 | T, 36 | Needle, 37 | Limit extends number, 38 | $Search extends $SearchMode, 39 | Acc extends unknown[][] = [] 40 | > = Limit extends 0 | never ? Acc 41 | 42 | : IsAny extends true 43 | ? IsAny extends true ? [] : NOT_FOUND 44 | 45 | : IsNeedle extends true 46 | ? [] 47 | 48 | : IsArray extends true 49 | ? Search, $Search> 50 | 51 | : T extends readonly unknown[] 52 | ? Search, $Search> 53 | 54 | : T extends GenericFree 55 | ? Search, $Search> 56 | 57 | : T extends Fn 58 | ? Search, $Search> 59 | 60 | : T extends { [k: PropertyKey]: unknown } 61 | ? Search, $Search> 62 | 63 | : NOT_FOUND; 64 | 65 | type Search< 66 | Limit extends number, 67 | Acc extends unknown[][], 68 | $I extends $Iterator, 69 | $Seach extends $SearchMode 70 | > = apply<$Seach['total'], [Acc, $I, apply<$Seach['shallow'], [Limit, $I]>]> 71 | 72 | 73 | type LIMIT = 0; 74 | type VALUE = 2; 75 | type PATH = 3; 76 | type $SEARCH = 4; 77 | 78 | interface $DeepSearchImpl extends $DeepSearch { 79 | type: unknown extends this[LIMIT] ? unknown[][] 80 | : GetPaths, Needle, Lossy, Checked<$SEARCH, this>, []> extends infer Paths 81 | ? Paths extends NOT_FOUND ? this['Acc'] 82 | : [ 83 | ...this['Acc'], 84 | ...Spread<{[K in keyof Paths]: [...Checked, ...Spread]}> 85 | ] 86 | : never; 87 | 88 | Acc: Checked<1, this>, 89 | } 90 | 91 | type FormatSelf = GetOrElse; 92 | type Spread = GetOrElse; 93 | -------------------------------------------------------------------------------- /src/FindReplace.ts: -------------------------------------------------------------------------------- 1 | import { FindPaths } from "./FindPaths"; 2 | import { Path } from "./types"; 3 | import { Next } from "./utils"; 4 | import { Type, A, B, C, Checked, apply, Last, partialRight } from "free-types-core"; 5 | import { Replace } from "./Replace"; 6 | import { Over } from "./Over"; 7 | 8 | export { FindReplace, $Callback as $ReplaceCallback } 9 | 10 | type $Callback = Type<[T, Path?]>; 11 | 12 | type FindReplace, From extends Path = [], Limit extends number = number> = 13 | FindPaths extends infer Queries 14 | ? [Queries] extends [never] 15 | ? T 16 | : Queries extends Path[] 17 | ? Fold 19 | : V extends Type<[Needle]> 20 | ? $WithUnaryCallback 21 | : V extends Type 22 | ? $WithBinaryCallback 23 | : never> 24 | : never 25 | : never 26 | 27 | interface $WithValue extends Type<[unknown, Path, number]> { 28 | type: unknown extends this[1] ? unknown 29 | : Replace, A, LastAvailable>, any> 30 | } 31 | 32 | interface $WithUnaryCallback> extends Type<[unknown, Path]> { 33 | type: unknown extends this[1] ? unknown 34 | : Over, A, V, any> 35 | } 36 | 37 | interface $WithBinaryCallback extends Type<[unknown, Path]> { 38 | type: unknown extends this[1] ? unknown 39 | : Over, A, 40 | partialRight], $Callback>, any> 41 | } 42 | 43 | // TODO: make free-types/Fold accept an index 44 | type Fold< 45 | T extends readonly $T['constraints'][1][], 46 | Init extends $T['constraints'][0] & $T['type'], 47 | $T extends Type<2 | 3>, 48 | Acc extends unknown = Init, 49 | I extends number = 0 50 | > = I extends T['length'] ? Acc 51 | : Fold, Next>; 52 | 53 | type LastAvailable = 54 | IsIndexOf extends true ? V[I] : Last; 55 | 56 | type IsIndexOf = 57 | `${I}` extends Extract ? true : false; -------------------------------------------------------------------------------- /src/Follow.ts: -------------------------------------------------------------------------------- 1 | import { self, Param, Output, Key, PathItem, Fn, NOT_FOUND } from './types' 2 | import { Type, inferArgs, Generic, apply } from 'free-types-core'; 3 | import { IsAny, Parameters } from './utils'; 4 | 5 | export { FollowPath, NOT_FOUND } 6 | 7 | type FollowPath = 8 | undefined extends I ? NOT_FOUND 9 | : I extends Output ? Data extends Fn ? ReturnType : NOT_FOUND 10 | : I extends Param ? Data extends Fn ? IfDefined[I['key']]> : NOT_FOUND 11 | : I extends Key ? 12 | Data extends ReadonlyArray 13 | ? I extends keyof Data ? IfDefined : NOT_FOUND 14 | : Data extends Record 15 | ? I extends keyof Data ? Data[I] : NOT_FOUND 16 | : NOT_FOUND 17 | : I extends self ? Self 18 | : I extends Type ? FreeTypeArgs 19 | : NOT_FOUND 20 | 21 | type IfDefined = 22 | IsAny extends true ? T 23 | : [T] extends [undefined] ? NOT_FOUND 24 | : T; 25 | 26 | type FreeTypeArgs = 27 | apply<$T, any[]> extends T 28 | ? T extends Generic<$T> 29 | ? inferArgs 30 | : NOT_FOUND 31 | : NOT_FOUND; -------------------------------------------------------------------------------- /src/Get.ts: -------------------------------------------------------------------------------- 1 | import { Query, ILens } from './types'; 2 | import { Next, MapOver, _, _$Optional } from './utils'; 3 | import { FollowPath, NOT_FOUND } from './Follow'; 4 | import { Lens } from './Lens'; 5 | import { Type, Checked, Optional, A, B, C, $partial, $apply } from 'free-types-core' 6 | 7 | export { Get, GetMulti, $Get, $GetMulti } 8 | 9 | type Get = 10 | _Get, Data, Self> 11 | 12 | type _Get< 13 | L extends ILens, 14 | Data, 15 | Self, 16 | I extends number = 0, 17 | F = FollowPath 18 | > = F extends NOT_FOUND ? never 19 | : Next extends L['path']['length'] ? F 20 | : _Get>; 21 | 22 | // naive implementation but it is not obvious that a custom traversal would perform better 23 | type GetMulti = 24 | MapOver>, $apply<[Data, Self]>> 25 | 26 | interface _$Get extends Type<[Query, unknown, unknown?]> { 27 | type: Get, B, Optional>> 28 | } 29 | 30 | type $Get = 31 | _$Optional<_$Get, [Q, _, Self]> 32 | 33 | interface _$GetMulti extends Type<[Query[], unknown, unknown?]> { 34 | type: GetMulti, B, Optional>> 35 | } 36 | 37 | type $GetMulti = 38 | _$Optional<_$GetMulti, [Qs, _, Self]> -------------------------------------------------------------------------------- /src/Lens.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'free-types-core'; 2 | import { PathItem, Query, ILens, QueryItem } from './types'; 3 | import { Next } from './utils'; 4 | 5 | export { Lens, $Lens } 6 | 7 | /** @internal */ 8 | type Lens = 9 | Q extends ILens ? Q 10 | : Q extends PathItem ? { __type_lenses: 'lens', path: [Q] } 11 | : Q extends readonly QueryItem[] ? { __type_lenses: 'lens', path: Flatten } 12 | : never ; 13 | 14 | interface $Lens extends Type<[Query], ILens> { 15 | type: this[0] extends Query ? Lens : ILens 16 | } 17 | 18 | type Flatten = 19 | I extends T['length'] ? R 20 | : T[I] extends PathItem 21 | ? Flatten, [...R, T[I]]> 22 | : T[I] extends ILens 23 | ? Flatten, [...R, ...T[I]['path']]> 24 | : never; -------------------------------------------------------------------------------- /src/Modify.ts: -------------------------------------------------------------------------------- 1 | import { self, Param, Output, Key, PathItem, Fn, Indexable } from './types' 2 | import { Next } from './utils' 3 | import { Type, apply } from 'free-types-core'; 4 | 5 | export { ModifyPath }; 6 | 7 | type ModifyPath = 8 | I extends Output ? Data extends Fn ? (...args: Parameters) => V : never 9 | : I extends Param ? Data extends Fn 10 | ? (...args: SetIndex>) => 11 | ReturnType 12 | : never 13 | : I extends number ? Data extends unknown[] ? SetIndex : never 14 | : I extends Key ? Data extends Indexable ? { 15 | [K in keyof Data as FilteredKeys] 16 | : K extends I | `${I & string}` ? V : Data[K]; 17 | } : never 18 | : I extends self ? V 19 | : I extends Type ? V extends unknown[] ? apply : never 20 | : never 21 | 22 | 23 | type SetIndex< 24 | A extends number, 25 | V, 26 | T extends unknown[], 27 | I extends number = 0, 28 | R extends unknown[] = [] 29 | > = number extends T['length'] ? T 30 | : I extends T['length'] ? R 31 | : SetIndex, [ 32 | ...R, I extends A ? V : T[I] 33 | ]>; 34 | 35 | type FilteredKeys = 36 | [unknown] extends [T[K]] 37 | ? string extends K ? never 38 | : number extends K ? never 39 | : symbol extends K ? never 40 | : K 41 | : K; -------------------------------------------------------------------------------- /src/Over.ts: -------------------------------------------------------------------------------- 1 | import { PathItem, Query, ILens } from './types' 2 | import { Type, apply } from 'free-types-core'; 3 | import { Next } from './utils'; 4 | import { ModifyPath } from './Modify'; 5 | import { FollowPath, NOT_FOUND } from './Follow'; 6 | import { Lens } from './Lens'; 7 | 8 | export { Over, $Over, ValidTransform }; 9 | 10 | type Over> = 11 | _Over, Data, Extract> 12 | 13 | 14 | type ValidTransform['path']> = 15 | $V extends Type ? 16 | Path extends [...any[], infer $T extends Type] 17 | ? RelatedTo<$T['constraints'], $V['constraints']> extends true 18 | ? Type<$V['constraints']['length'], $T['constraints']> 19 | : Audit<$T['constraints'], $V['constraints'], $T['constraints']> 20 | : Path extends [...any[], infer $T extends Type, infer N extends number] 21 | ? RelatedTo<[$T['constraints'][N]], $V['constraints']> extends true 22 | ? Type<1, $T['constraints'][N]> 23 | : Audit<[$T['constraints'][N]], $V['constraints'], $T['constraints'][N]> 24 | : Type 25 | : Type 26 | 27 | 28 | type Audit = 29 | V['length'] extends T['length'] 30 | ? I extends T['length'] 31 | ? Type 32 | : Audit, [...C, RelatedTo extends true ? V[I]: Unrelated]> 33 | : Type 34 | 35 | type _Over< 36 | L extends ILens, 37 | Data, 38 | $V extends Type, 39 | I extends number = 0, 40 | C extends PathItem = L['path'][I], 41 | F = FollowPath 42 | > = I extends L['path']['length'] ? apply<$V, [Data]> 43 | : F extends NOT_FOUND ? Data 44 | : ModifyPath>>; 45 | 46 | interface $Over extends Type<1> { 47 | type: _Over, this[0], $V> 48 | } 49 | 50 | type Unrelated = [V, 'and', T, 'are unrelated' ]; 51 | 52 | type RelatedTo = T extends U ? true : U extends T ? true : false; -------------------------------------------------------------------------------- /src/Replace.ts: -------------------------------------------------------------------------------- 1 | import { PathItem, Query, ILens } from './types' 2 | import { Type } from 'free-types-core'; 3 | import { Next, Last } from './utils'; 4 | import { ModifyPath } from './Modify'; 5 | import { Lens } from './Lens'; 6 | import { FollowPath, NOT_FOUND } from './Follow'; 7 | 8 | export { Replace, $Replace }; 9 | 10 | type Replace> = 11 | [Q] extends [never] ? Data : _Replace, Data, V> 12 | 13 | type ValidValue['path']> = 14 | [Q] extends [never] ? unknown 15 | : Path extends [...any[], infer $T extends Type] ? $T['constraints'] 16 | : Path extends [...any[], infer $T extends Type, infer N extends number] ? $T['constraints'][N] 17 | : unknown 18 | 19 | type _Replace< 20 | L extends ILens, 21 | Data, 22 | V, 23 | I extends number = 0, 24 | C extends PathItem = L['path'][I], 25 | F = FollowPath, 26 | > = 27 | I extends L['path']['length'] ? V 28 | : F extends NOT_FOUND ? Data 29 | : ModifyPath>>; 30 | 31 | interface $Replace> extends Type<1> { 32 | type: _Replace, this[0], V> 33 | } -------------------------------------------------------------------------------- /src/Search/ExhaustiveSearch.ts: -------------------------------------------------------------------------------- 1 | import { Type, apply, Checked, Lossy } from "free-types-core"; 2 | import { $SearchMode, $TotalSearch, $ShallowSearch, ShallowSearchResult, ShouldStop, NonEmpty, IsNeedle, $DeepSearch } from "./types"; 3 | import { Next, Prev } from "../utils"; 4 | import { $Iterator } from "./Iterators"; 5 | 6 | export { SearchMode } 7 | 8 | type SearchMode = { 9 | total: $Total, 10 | shallow: $Shallow 11 | deep: $Deep 12 | } 13 | 14 | interface $Total extends $TotalSearch { 15 | type: unknown extends this[0] ? never : TotalSeach< 16 | Needle, 17 | Checked<0, this>, 18 | Checked<1, this>, 19 | Checked<2, this>, 20 | SearchMode 21 | > 22 | } 23 | 24 | interface $Shallow extends $ShallowSearch { 25 | type: unknown extends this[0] ? ShallowSearchResult 26 | : ShallowSearch, Checked<1, this>> 27 | } 28 | 29 | type TotalSeach< 30 | Needle, 31 | Acc extends unknown[][], 32 | $I extends $Iterator, 33 | Shallow extends ShallowSearchResult, 34 | $Search extends $SearchMode, 35 | I extends number = 0, 36 | Deep extends unknown[][] = [] 37 | > = ShouldStop extends true 38 | ? NonEmpty> 39 | : TotalSeach< 40 | Needle, Acc, $I, Shallow, $Search, Next, 41 | apply<$I['path'], [I]> extends infer Path 42 | ? Path extends unknown[] 43 | ? Path extends Shallow['total'][number] ? Deep : [ 44 | ...MergeDeduplicate, 45 | ...apply<$Search['deep'], [Shallow['limit'], Acc, apply<$I['value'], [I]>, Path, $Search]> 46 | ] 47 | : never 48 | : never 49 | >; 50 | 51 | type MergeDeduplicate< 52 | A extends unknown[], 53 | B extends unknown[], 54 | I extends number = 0, 55 | R extends unknown[] = [] 56 | > = I extends A['length'] ? [...R, ...B] 57 | : MergeDeduplicate, A[I] extends B[number] ? R : [...R, A[I]]> 58 | 59 | type PrependType = 60 | I extends T['length'] ? R 61 | : PrependType, 62 | T[I][0] extends Type 63 | ? [T[I][0]] extends R[number] 64 | ? [...R, T[I]] 65 | : [...R, [T[I][0]], T[I]] 66 | : [...R, T[I]] 67 | >; 68 | 69 | type ShallowSearch< 70 | Needle, 71 | Limit extends number, 72 | $I extends $Iterator, 73 | I extends number = 0, 74 | TotalMatch extends unknown[] = [], 75 | PartialMatch extends unknown[] = [], 76 | > = ShouldStop extends true ? { partial: PartialMatch, total: TotalMatch, limit: Limit } 77 | : true extends IsNeedle, Needle> 78 | ? apply<$I['path'], [I]> extends infer P extends unknown[] 79 | ? IsNeedle, Needle> extends true 80 | ? ShallowSearch, $I, Next,[...TotalMatch, P], PartialMatch> 81 | : ShallowSearch, $I, Next, TotalMatch, [...PartialMatch, P]> 82 | : never 83 | : ShallowSearch, TotalMatch>; -------------------------------------------------------------------------------- /src/Search/Iterators/Array.ts: -------------------------------------------------------------------------------- 1 | import { A, Const } from "free-types-core" 2 | import { $Accessor, $Iterator } from "./types" 3 | 4 | export { $Array } 5 | 6 | interface $Array extends $Iterator { 7 | value: Const 8 | path: Const<[number]> 9 | done: $Done 10 | } 11 | 12 | interface $Done extends $Accessor { 13 | type: A extends 1 ? true : false 14 | } -------------------------------------------------------------------------------- /src/Search/Iterators/Fn.ts: -------------------------------------------------------------------------------- 1 | import { A } from "free-types-core"; 2 | import { Fn, Output, Param } from "../../types"; 3 | import { $Accessor, $Iterator } from "./types" 4 | import { $Done, $GetValue } from "./Tuple"; 5 | import { Parameters, Prev } from "../../utils"; 6 | 7 | export { $Fn }; 8 | 9 | interface $Fn, ReturnType]> extends $Iterator { 10 | value: $GetValue

11 | path: $GetPath> 12 | done: $Done

13 | } 14 | 15 | interface $GetPath extends $Accessor { 16 | type: A extends Last ? [Output] : [Param>] 17 | } 18 | -------------------------------------------------------------------------------- /src/Search/Iterators/Free.ts: -------------------------------------------------------------------------------- 1 | import { unwrap, Unwrapped, Type, A } from "free-types-core" 2 | import { $Accessor, $Iterator } from "./types" 3 | 4 | export { $Free } 5 | 6 | interface $Free> extends $Iterator { 7 | value: $GetValue 8 | path: $GetPath 9 | done: $Done 10 | } 11 | 12 | interface $GetValue extends $Accessor { 13 | type: Args[A] 14 | } 15 | 16 | interface $GetPath<$T extends Type> extends $Accessor { 17 | type: [$T, A] 18 | } 19 | 20 | interface $Done extends $Accessor { 21 | type: A extends Args['length'] ? true : false 22 | } -------------------------------------------------------------------------------- /src/Search/Iterators/Struct.ts: -------------------------------------------------------------------------------- 1 | import { A } from "free-types-core"; 2 | import { Union2Tuple } from "../../utils"; 3 | import { $Accessor, $Iterator } from "./types" 4 | 5 | export { $Struct } 6 | 7 | interface $Struct> extends $Iterator { 8 | value: $GetValue 9 | path: $GetPath 10 | done: $Done 11 | } 12 | 13 | type GetKeys = Extract, (keyof T)[]> 14 | 15 | interface $GetValue extends $Accessor { 16 | type: T[Keys[A]] 17 | } 18 | 19 | interface $GetPath extends $Accessor { 20 | type: [Keys[A]] 21 | } 22 | 23 | interface $Done extends $Accessor { 24 | type: A extends Keys['length'] ? true : false 25 | } -------------------------------------------------------------------------------- /src/Search/Iterators/Tuple.ts: -------------------------------------------------------------------------------- 1 | import { A } from "free-types-core" 2 | import { $Accessor, $Iterator } from "./types" 3 | 4 | export { $Tuple, $GetValue, $Done } 5 | 6 | interface $Tuple extends $Iterator { 7 | value: $GetValue 8 | path: $GetPath 9 | done: $Done 10 | } 11 | 12 | interface $GetValue extends $Accessor { 13 | type: T[A] 14 | } 15 | 16 | interface $GetPath extends $Accessor { 17 | type: [A] 18 | } 19 | 20 | interface $Done extends $Accessor { 21 | type: A extends T['length'] ? true : false 22 | } -------------------------------------------------------------------------------- /src/Search/Iterators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { $Fn } from './Fn'; 3 | export { $Struct } from './Struct'; 4 | export { $Tuple } from './Tuple'; 5 | export { $Array } from './Array'; 6 | export { $Free } from './Free'; -------------------------------------------------------------------------------- /src/Search/Iterators/types.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "free-types-core" 2 | 3 | export { $Iterator, $Accessor } 4 | 5 | type $Iterator = { 6 | value: $Accessor, 7 | path: $Accessor, 8 | done: $Accessor 9 | }; 10 | 11 | type $Accessor = Type<[number], R>; -------------------------------------------------------------------------------- /src/Search/StandardSearch.ts: -------------------------------------------------------------------------------- 1 | import { apply, Checked, Lossy } from "free-types-core"; 2 | import { $SearchMode, $TotalSearch, $ShallowSearch, ShallowSearchResult, ShouldStop, NonEmpty, IsNeedle, $DeepSearch } from "./types"; 3 | import { Next, Prev } from "../utils"; 4 | import { $Iterator } from "./Iterators"; 5 | 6 | export { SearchMode } 7 | 8 | type SearchMode = { 9 | total: $Total, 10 | shallow: $Shallow 11 | deep: $Deep 12 | } 13 | 14 | interface $Total extends $TotalSearch { 15 | type: unknown extends this[0] ? never : TotalSearch< 16 | Needle, 17 | Checked<0, this>, 18 | Checked<1, this>, 19 | Checked<2, this>, 20 | SearchMode 21 | > 22 | } 23 | 24 | interface $Shallow extends $ShallowSearch { 25 | type: unknown extends this[0] ? ShallowSearchResult 26 | : ShallowSearch, Checked<1, this>> 27 | } 28 | 29 | type TotalSearch< 30 | Needle, 31 | Acc extends unknown[][], 32 | $I extends $Iterator, 33 | Shallow extends ShallowSearchResult, 34 | $Search extends $SearchMode, 35 | I extends number = 0, 36 | Deep extends unknown[][] = [], 37 | Path extends unknown[] = apply<$I['path'], [I]>, 38 | > = ShouldStop extends true 39 | ? NonEmpty<[...Acc, ...Shallow['total'], ...Deep]> 40 | : TotalSearch< 41 | Needle, Acc, $I, Shallow, $Search, Next, 42 | Path extends Shallow['total'][number] ? Deep 43 | : [...Deep, ...apply<$Search['deep'], [Shallow['limit'], Acc, apply<$I['value'], [I]>, Path, $Search]>] 44 | >; 45 | 46 | type ShallowSearch< 47 | Needle, 48 | Limit extends number, 49 | $I extends $Iterator, 50 | I extends number = 0, 51 | R extends unknown[] = [], 52 | > = ShouldStop extends true ? { partial: never, total: R, limit: Limit } 53 | : IsNeedle, Needle> extends true 54 | ? ShallowSearch, $I, Next,[...R, apply<$I['path'], [I]>]> 55 | : ShallowSearch, R>; -------------------------------------------------------------------------------- /src/Search/types.ts: -------------------------------------------------------------------------------- 1 | import { Type, apply } from "free-types-core" 2 | import { $Iterator } from "./Iterators"; 3 | import { NOT_FOUND, BaseType, self } from "../types"; 4 | import { GetOrElse, IsAny } from "../utils"; 5 | 6 | export type $SearchMode = { 7 | total: $TotalSearch 8 | shallow: $ShallowSearch 9 | deep: $DeepSearch 10 | }; 11 | 12 | export type $DeepSearch = Type< 13 | [ 14 | Limit: number, 15 | Acc: unknown[][], 16 | Value: unknown, 17 | Path: unknown[], 18 | Search: $SearchMode 19 | ], 20 | unknown[][] 21 | >; 22 | 23 | export type $TotalSearch = Type<[ 24 | Acc: unknown[][], 25 | $I: $Iterator, 26 | S: ShallowSearchResult 27 | ]>; 28 | 29 | export type $ShallowSearch = Type< 30 | [Limit: number, $I: $Iterator], 31 | ShallowSearchResult 32 | >; 33 | 34 | export type ShallowSearchResult = { 35 | partial: unknown[][], 36 | total: unknown[][], 37 | limit: number 38 | }; 39 | 40 | export type ShouldStop = 41 | Limit extends 0 | never ? true : apply<$I['done'], [I]>; 42 | 43 | export type NonEmpty = GetOrElse; 44 | 45 | export type IsNeedle = 46 | IsAny extends true ? IsAny extends true ? true : false 47 | : IsAny extends true ? false 48 | : [T] extends [never] ? [Needle] extends [never] ? true : MatchAll extends true ? true : false 49 | : [Needle] extends [never] ? [T] extends [never] ? true : false 50 | : unknown extends Needle ? unknown extends T ? true : false 51 | : T extends Needle ? true 52 | : MatchAll extends true ? T extends BaseType ? true : boolean 53 | : false; 54 | 55 | export type MatchAll = Needle extends self ? true : false; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Get, GetMulti, $Get, $GetMulti } from './Get' 2 | // export { ReplaceMulti } from './ReplaceMulti' 3 | export { Replace, $Replace } from './Replace' 4 | export { Over, $Over } from './Over' 5 | export { FindPaths } from './FindPaths' 6 | export { FindPath } from './FindPath' 7 | export { FindReplace, $ReplaceCallback } from './FindReplace' 8 | export * from './placeholders' 9 | export { self, Output, QueryItem, Query, Path, Param } from './types' 10 | export { free } from 'free-types-core'; 11 | export { inferArgs } from 'free-types-core'; 12 | export { Audit, Successful } from './Audit'; 13 | import { Audit } from './Audit' 14 | import { Lens as CreateLens, $Lens } from './Lens'; 15 | import { ILens, Query } from './types' 16 | 17 | export { Lens, $Lens } 18 | 19 | type Lens< 20 | Q extends Check = never, 21 | Model = never, 22 | Check = CheckQuery 23 | > = [Q] extends [never] ? ILens : CreateLens 24 | 25 | type CheckQuery = 26 | [Model] extends [never] ? Query 27 | : Q extends Query ? Audit 28 | : Query; -------------------------------------------------------------------------------- /src/placeholders.ts: -------------------------------------------------------------------------------- 1 | import { Query, Param, Output, PathItem, Path, ILens } from './types'; 2 | import { Lens } from './Lens'; 3 | 4 | export { r, a, b, c, d, e, f, g, h, i, j }; 5 | 6 | type a = Param<0>; 7 | type b = Param<1>; 8 | type c = Param<2>; 9 | type d = Param<3>; 10 | type e = Param<4>; 11 | type f = Param<5>; 12 | type g = Param<6>; 13 | type h = Param<7>; 14 | type i = Param<8>; 15 | type j = Param<9>; 16 | 17 | type r

= 18 | [P] extends [never] ? Output 19 | : P extends PathItem ? Lens<[P, Output]> 20 | : P extends Path ? Lens<[...P, Output]> 21 | : P extends ILens ? Lens<[...P['path'], Output]> 22 | : never; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'free-types-core'; 2 | 3 | export { self, Query, QueryItem, Param, Output, Key, PathItem, Path, Fn, Indexable, Lens as ILens, NOT_FOUND, BaseType } 4 | 5 | type BaseType = string | number | boolean | symbol | undefined | void ; 6 | 7 | type PathItem = Param | Output | self | Key | Type; 8 | type Lens = { __type_lenses: 'lens', path: Path } 9 | type QueryItem = PathItem | Lens; 10 | type Path = readonly PathItem[] 11 | type Query = QueryItem | readonly QueryItem[]; 12 | type Param = { __type_lenses: 'param', key: K }; 13 | type Output = { __type_lenses: 'output' }; 14 | 15 | type self = { __type_lenses: 'self' }; 16 | type Fn = (...args: any[]) => unknown 17 | type Indexable = { [k: Key]: any } & { readonly [Symbol.toStringTag]?: never }; 18 | type Key = number | string | symbol; 19 | declare const NOT_FOUND: unique symbol; 20 | type NOT_FOUND = typeof NOT_FOUND; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { TypesMap, Generic } from 'free-types-core'; 2 | import { Next } from 'free-types-core/dist/utils'; 3 | import { Fn } from './types' 4 | 5 | export { Next, Prev, Subtract, Last, Init, IsAny, IsUnknown } from 'free-types-core/dist/utils'; 6 | export { MapOver } from 'free-types/essential/mappables/MapOver'; 7 | export { _ } from 'free-types/essential/_partial'; 8 | export { _$Optional} from 'free-types/essential/adapters/$Optional'; 9 | 10 | export type IsArray = any[] extends T ? true : never[] extends T ? true : false; 11 | 12 | export type GetOrElse = T extends U ? T : E; 13 | 14 | export type GenericFree = Exclude< 15 | Generic, 16 | Fn | readonly unknown[] | Record 17 | >; 18 | 19 | export type SequenceTo = 20 | I extends N ? R | I 21 | : SequenceTo, R | I>; 22 | 23 | export type ToNumber = T extends `${infer I extends number}` ? I : never; 24 | 25 | export type Parameters = P extends any ? unknown[] extends P ? never : P : never; 67 | 68 | export type PickUnionMember< 69 | T, 70 | HOFs = T extends any ? (a: () => T) => void : never, 71 | Overloads = [HOFs] extends [(a: infer I) => any] ? I : never, 72 | > = Overloads extends () => (infer R) ? R : never; 73 | 74 | export type Union2Tuple> = 75 | [U] extends [never] ? [] 76 | : [...Union2Tuple>, T]; -------------------------------------------------------------------------------- /tests/FindPaths.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'free-types-core'; 2 | import { test } from 'ts-spec'; 3 | import { a, b, r, FindPath, FindPaths, free, self } from '../src/'; 4 | 5 | declare const needle: unique symbol; 6 | type needle = typeof needle; 7 | 8 | type Haystack = Map needle) => void, 'bar'] }>; 9 | 10 | test('FindPath', t => [ 11 | t.equal, [self]>(), 12 | t.equal, [self]>(), 13 | t.equal, never>(), 14 | t.equal, never>(), 15 | t.equal, [self]>(), 16 | t.equal, never>(), 17 | t.equal, ['a']>(), 18 | t.equal, ['a']>(), 19 | t.equal, ['a']>(), 20 | t.equal, ['b']>(), 21 | t.equal, ['b']>(), 22 | t.equal, ['b', 'c']>(), 23 | t.equal, [number]>(), 24 | t.equal, [number, 'a']>(), 25 | t.equal, [0, 0]>(), 26 | t.equal, [0, 0]>(), 27 | t.equal, [1, 0]>(), 28 | t.equal, needle>, [free.Map, 1]>(), 29 | t.equal, needle>, [free.Map, 1, 'a']>(), 30 | t.equal void, needle>, [a]>(), 31 | t.equal needle, needle>, [r]>(), 32 | t.equal void) => void, needle>, [a, a]>(), 33 | t.equal { a: needle }, needle>, [r, 'a']>(), 34 | t.equal, [free.Map, 1, "foo", 0, a, r]>() 35 | ]); 36 | 37 | test('FindPath takes a From argument as starting point', t => [ 38 | t.equal< 39 | FindPath<{ a: needle, b: [needle] }, needle, ['b']>, 40 | ['b', 0] 41 | >() 42 | ]) 43 | 44 | test('FindPath', t => [ 45 | t.equal, [self]>(), 46 | t.equal, [number]>(), 47 | t.equal, never>(), 48 | t.equal, [0]>(), 49 | t.equal, [0, 0]>(), 50 | t.equal, [1, 0]>(), 51 | t.equal, [0]>(), 52 | t.equal, [0]>(), 53 | 54 | t.equal, ['a']>(), 55 | t.equal, ['b', 'c']>(), 56 | t.equal, ['a', 'b']>(), 57 | 58 | t.equal, ['a', 0]>(), 59 | t.equal, [0, 'a']>(), 60 | 61 | t.equal, needle>, [free.Map, 1]>(), 62 | t.equal>, needle>, [free.Map, 1, free.Set, 0]>(), 63 | 64 | t.equal needle, needle>, [r]>(), 65 | t.equal () => needle, needle>, [r, r]>(), 66 | t.equal [needle], needle>, [r, 0]>(), 67 | 68 | t.equal void, needle>, [a]>(), 69 | t.equal void) => void, needle>, [a, a]>(), 70 | t.equal void) => void, needle>, [a, a, 0]>(), 71 | ]) 72 | 73 | test('FindPath: stops at the first instance of Needle', t => [ 74 | t.equal, [1]>(), 75 | t.equal, [0, 0]>(), 76 | 77 | t.equal, ['b']>(), 78 | t.equal, ['a', 'b']>(), 79 | 80 | t.equal>, needle>, [$Triple, 1]>(), 81 | 82 | t.equal needle, needle>, [a]>(), 83 | t.equal needle, b: needle) => needle, needle>, [b]>(), 84 | t.equal needle, needle>, [a]>(), 85 | t.equal needle, needle>, [r]>(), 86 | 87 | ]) 88 | 89 | test('FindPath: deal with `any`', t => [ 90 | t.equal, [self]>(), 91 | t.equal, [number]>(), 92 | t.equal, never>(), 93 | t.equal, never>(), 94 | t.equal, [1]>(), 95 | t.equal, never>(), 96 | t.equal, [number]>(), 97 | ]) 98 | 99 | test('FindPath: deal with `never`', t => [ 100 | t.equal, [self]>(), 101 | t.equal, never>(), 102 | t.equal, never>(), 103 | t.equal, [1]>(), 104 | t.equal, never>(), 105 | t.equal, [number]>(), 106 | t.equal, never>(), 107 | t.equal, ['a']>(), 108 | ]) 109 | 110 | test('FindPath: deal with unknown', t => [ 111 | t.equal, [self]>(), 112 | t.equal, [0]>(), 113 | t.equal, never>(), 114 | ]) 115 | 116 | test('FindPaths can find paths which are not leaves', t => [ 117 | t.equal< 118 | FindPaths<[[1, 2], 3]>, 119 | [[1], [0], [0, 0], [0, 1]] 120 | >(), 121 | t.equal< 122 | FindPaths<[[1, 2], [3, 4]]>, 123 | [[0], [1], [0, 0], [0, 1], [1, 0], [1, 1]] 124 | >(), 125 | t.equal< 126 | FindPaths<{a: [1, 2], b: 3}>, 127 | [['b'], ['a'], ['a', 0], ['a', 1]] 128 | >(), 129 | t.equal< 130 | FindPaths<{ a: { b: 'foo' } }>, 131 | [['a'], ['a', 'b']] 132 | >(), 133 | t.equal< 134 | FindPaths<[1, 2, [3, 4]]>, 135 | [[0], [1], [2], [2, 0], [2, 1]] 136 | >(), 137 | t.equal< 138 | FindPaths>>, [ 139 | [free.Map], 140 | [free.Map, 0], 141 | [free.Map, 1], 142 | [free.Map, 1, free.Set], 143 | [free.Map, 1, free.Set, 0] 144 | ]>(), 145 | t.equal< 146 | FindPaths<(f: (arg: string) => void) => [number]>, [ 147 | [a], 148 | [r], 149 | [a, a], 150 | [a, r], 151 | [r, 0], 152 | ]>(), 153 | ]) 154 | 155 | class Triple { 156 | constructor(private a: A, private b: B, private c: C) {} 157 | } 158 | 159 | interface $Triple extends Type<3> { 160 | type: Triple 161 | } 162 | 163 | declare module 'free-types-core' { 164 | export interface TypesMap { 165 | Triple: $Triple 166 | } 167 | } 168 | 169 | test('FindPaths', t => [ 170 | t.equal, []>(), 171 | t.equal, []>(), 172 | t.equal, ['a']>(), 173 | t.equal, [[0], [1], [2]]>(), 174 | t.equal< 175 | FindPaths<[1, [10, 20], 300], number>, 176 | [[0], [2], [1, 0], [1, 1]] 177 | >(), 178 | t.equal, [[0], [2]]>(), 179 | t.equal, [['a'], ['b']]>(), 180 | t.equal, [['a'], ['b']]>(), 181 | t.equal void, number>, [[a], [b]]>(), 182 | t.equal [needle, needle], needle>, [[r, 0], [r, 1]]>(), 183 | t.equal< 184 | FindPaths<(f: (a: 1) => void, b: 2) => void, number>, 185 | [[b], [a, a]] 186 | >(), 187 | t.equal< 188 | FindPaths<(f: (a: [1]) => void, b: 2) => void, number>, 189 | [[b], [a, a, 0]] 190 | >(), 191 | t.equal, number>, [[free.Map, 0], [free.Map, 1]]>(), 192 | t.equal< 193 | FindPaths>, number>, 194 | [[free.Map, 0], [free.Map, 1, free.Map, 0], [free.Map, 1, free.Map, 1]] 195 | >() 196 | ]) 197 | 198 | 199 | test('FindPaths Limit', t => [ 200 | t.equal, [[0], [1]]>(), 201 | t.equal< 202 | FindPaths<[1, [10, 20], 300], number, [], 3>, 203 | [[0], [2], [1, 0]] 204 | >(), 205 | t.equal< 206 | FindPaths>, number, [], 2>, 207 | [[free.Map, 0], [free.Map, 1, free.Map, 0]] 208 | >() 209 | ]) -------------------------------------------------------------------------------- /tests/FindReplace.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'ts-spec'; 2 | import { FindReplace, $ReplaceCallback } from '../src/'; 3 | import { Type, A, B, Optional, Last } from "free-types-core"; 4 | import { Next } from '../src/utils'; 5 | 6 | interface $Next extends Type<[number], number> { type: Next> } 7 | 8 | interface $Callback extends $ReplaceCallback { 9 | type: this['prop'] extends 'a' ? 42 10 | : this['prop'] extends 'b' ? 2001 11 | : never 12 | prop: Last> 13 | } 14 | 15 | test('FindReplace', t => [ 16 | t.equal, 42>(), 17 | t.equal, [42]>(), 18 | t.equal, [42, 'b']>(), 19 | t.equal, { a: 42, b: 'b' }>(), 20 | t.equal, { a: 42, b: 'b' }>(), 21 | ]) 22 | 23 | test('FindReplace with unary free type', t => [ 24 | t.equal, { a: 2, b: 3 }>(), 25 | ]) 26 | 27 | test('FindReplace with binary free type', t => [ 28 | t.equal, { a: 42, b: 2001 }>(), 29 | ]) 30 | 31 | test('FindReplace multiple needles', t => [ 32 | t.equal, { a: 42, b: 42 }>(), 33 | ]) 34 | 35 | test('FindReplace multiple needles and values', t => [ 36 | t.equal, [ a: 42, b: 2001 ]>(), 37 | t.equal, [ a: 42, b: 2001, c: 3 ]>(), 38 | t.equal, [ a: 42, b: 2001, c: 2001 ]>(), 39 | ]) 40 | 41 | test('FindReplace Limit', t => [ 42 | t.equal, [ a: 42, b: 2 ]>(), 43 | ]) 44 | 45 | test('FindReplace no match', t => [ 46 | t.equal, { a: 1, b: 2 }>(), 47 | ]) 48 | 49 | // @ts-expect-error: Values[] can't be empty 50 | {type Fail = FindReplace<{ a: 1, b: 2 }, number, []>} -------------------------------------------------------------------------------- /tests/Get.ts: -------------------------------------------------------------------------------- 1 | import { apply } from 'free-types-core'; 2 | import { test, Context } from 'ts-spec'; 3 | import { Lens, a, b, r, Get, $Get, GetMulti, $GetMulti, free, self } from '../src/'; 4 | 5 | declare const needle: unique symbol; 6 | type needle = typeof needle; 7 | 8 | const found = (t: Context) => t.equal(); 9 | 10 | type Haystack = Map needle) => void, 'bar'] }>; 11 | 12 | test('Get: return `never` when needle is not found', t => [ 13 | t.never>(), 14 | t.never>(), 15 | t.never>(), 16 | t.never>(), 17 | t.never>(), 18 | t.never unknown>>(), 19 | t.equal<[never], GetMulti<[[free.Function]], Haystack>>() 20 | ]) 21 | 22 | test('bare Get: tuple, object', t => [ 23 | found(t)>(), 24 | found(t)>(), 25 | ]); 26 | 27 | test('Get `any`', t => [ 28 | t.any>(), 29 | t.any>(), 30 | t.any unknown>>(), 31 | t.any any>>(), 32 | t.any>>(), 33 | ]) 34 | 35 | test('bare Get: function', t => [ 36 | found(t) void>>(), 37 | found(t) needle>>(), 38 | found(t), [1, () => needle]>>(), 39 | found(t), (arg1: () => needle) => void>>(), 40 | t.equal, needle | undefined>() 45 | ]); 46 | 47 | test('tuple Get: tuple, object', t => [ 48 | found(t)>(), 49 | found(t)>(), 50 | found(t)>(), 51 | found(t)>(), 52 | ]); 53 | 54 | test('readonly tuple Get', t => [ 55 | found(t)>(), 56 | ]); 57 | 58 | test('path Get: tuple, object', t => [ 59 | found(t), [needle, 2, 3]>>(), 60 | found(t), { a: needle, b: 2 }>>(), 61 | found(t), [1, [needle, 2], 3]>>(), 62 | found(t), { a: [1, needle], b: 2 }>>() 63 | ]); 64 | 65 | test('tuple Get: function', t => [ 66 | found(t) void]>>(), 67 | found(t) needle, b: 2 }>>(), 68 | found(t)], {a: 1, b: [2, (a: string) => needle]}>>(), 69 | found(t) needle]}>>(), 70 | found(t) needle) => void>>() 71 | ]); 72 | 73 | test('path Get', t => [ 74 | found(t), (a: needle) => void>>(), 75 | found(t), () => needle>>(), 76 | found(t)]>, [1, () => needle]>>(), 77 | found(t), [0, (a: needle) => void]>>(), 78 | found(t), { a: () => needle, b: 2 }>>(), 79 | found(t)]>, {a: 1, b: [2, (a: string) => needle]}>>(), 80 | found(t), {a: 1, b: [2, (a: string) => needle]}>>(), 81 | found(t), (arg1: () => needle) => void>>() 82 | ]); 83 | 84 | test('Get self', t => [ 85 | found(t), [], needle>>(), 86 | found(t)>(), 87 | found(t)>(), 88 | found(t)>(), 89 | ]) 90 | 91 | test('Get free type', t => 92 | found(t)>>(), 93 | ) 94 | 95 | test('GetMulti', t => { 96 | type Model = { a: {foo: 1}, b: [false, [2]], c: Set<3> } 97 | type Paths = [['a', 'foo'], ['b', 1, 0], ['c', free.Set, 0]] 98 | type Test = GetMulti; 99 | return t.equal() 100 | }) 101 | 102 | test('$Get produces a free type expecting Data', t => { 103 | type $Action = $Get<'foo'>; 104 | type Data = { foo: needle }; 105 | type Result = apply<$Action, [Data]> 106 | 107 | return t.equal() 108 | }) 109 | 110 | test('$Get produces a free type expecting Data - Self', t => { 111 | type $Action = $Get; 112 | type Data = { foo: needle }; 113 | type Self = needle; 114 | type Result = apply<$Action, [Data, Self]> 115 | 116 | return t.equal() 117 | }) 118 | 119 | test('$GetMulti produces a free type expecting Data', t => { 120 | type $Action = $GetMulti<['foo', ['bar', 2]]>; 121 | type Data = { foo: 'hello', bar: [0, 1, 'world'] }; 122 | type Result = apply<$Action, [Data]> 123 | 124 | return t.equal() 125 | }) -------------------------------------------------------------------------------- /tests/Lens.ts: -------------------------------------------------------------------------------- 1 | import { test, Context } from 'ts-spec'; 2 | import { Lens, a, b, r, Output, Get, Replace, free, self, QueryItem, Audit, Successful } from '../src/'; 3 | 4 | test('r', t => [ 5 | t.equal(), 6 | t.equal, Lens<[a, r]>>(), 7 | t.equal, Lens<[a, b, r]>>(), 8 | t.equal, Lens<[r, r]>>(), 9 | t.equal>, Lens<[a, r, r]>>() 10 | ]) 11 | 12 | test('Lens can take a standalone path item or a tupe', t => [ 13 | t.equal, Lens<['foo']>>(), 14 | t.equal, Lens<[1]>>(), 15 | t.equal, Lens<[a]>>(), 16 | t.equal, Lens<[r]>>(), 17 | t.equal, Lens<[self]>>(), 18 | ]); 19 | 20 | test('Lens idempotence', t => 21 | t.equal>, Lens<[a, r]>>() 22 | ) 23 | 24 | test('Lens composition', t => 25 | t.equal< 26 | Lens<[Lens<1>, Lens<['a', 2, r]>]>, 27 | Lens<[1, ...['a', 2, r]]> 28 | >() 29 | ) 30 | 31 | test('laws: identity', t => { 32 | type FocusName = Lens<'name'>; 33 | type User = { name: 'foo' }; 34 | 35 | return t.equal>, User>() 36 | }) 37 | 38 | test('laws: retention', t => { 39 | type FocusName = Lens<'name'>; 40 | type User = { name: 'foo' }; 41 | 42 | return t.equal>, 'bar'>() 43 | }) 44 | 45 | test('laws: recency', t => { 46 | type FocusName = Lens<'name'>; 47 | type User = { name: 'foo' }; 48 | 49 | return t.equal, 'baz'>>, 'baz'>() 50 | }) 51 | 52 | const OK = (t: Context) => t.equal(); 53 | 54 | test('Flat Lens type checking', t => [ 55 | OK(t)>(), 56 | OK(t)>(), 57 | OK(t)>(), 58 | OK(t)>>(), 59 | OK(t) }>>(), 60 | OK(t) unknown>>(), 61 | OK(t)>(), 66 | 67 | t.equal< 68 | Audit<'b', { a: [1, 2, 3] }>, 69 | 'a' 70 | >(), 71 | t.equal< 72 | Audit<['a', 0, 0], { a: [1, 2, 3] }>, 73 | ['a', 0] 74 | >(), 75 | t.equal< 76 | Audit<['a', 0, 0, 0], { a: [1, 2, 3] }>, 77 | ['a', 0] 78 | >(), 79 | t.equal< 80 | Audit<['b'], { a: [1, 2, 3] }>, 81 | ['a'] 82 | >(), 83 | t.equal< 84 | Audit<['a', 'b'], { a: [1, 2, 3] }>, 85 | ['a', 0 | 1 | 2] 86 | >(), 87 | t.equal< 88 | Audit<['a', 'b'], { a: { c: 1 } }>, 89 | ['a', 'c'] 90 | >(), 91 | t.equal< 92 | Audit<['a', 'b', 'd'], { a: { b: {c: 1 } } }>, 93 | ['a', 'b', 'c'] 94 | >(), 95 | t.equal< 96 | Audit<['a', 'b', 'c', 'e'], { a: { b: { c: { d: 1} } } }>, 97 | ['a', 'b', 'c', 'd'] 98 | >(), 99 | t.equal< 100 | Audit<['a', 'b'], { a: Map }>, 101 | ['a', free.Map] 102 | >(), 103 | t.equal< 104 | Audit<['a', 'b'], { a: (...args: any[]) => unknown }>, 105 | ['a', a | r] 106 | >(), 107 | t.equal< 108 | Audit<[free.Set], Map>, 109 | [free.Map] 110 | >(), 111 | t.equal< 112 | Audit<[1, free.Set], [0, Map]>, 113 | [1, free.Map] 114 | >(), 115 | t.equal< 116 | Audit<[free.Map, 2], Map>, 117 | [free.Map, 0 | 1] 118 | >(), 119 | t.equal< 120 | Audit<[b], (a: any) => unknown>, 121 | [a | r] 122 | >(), 123 | ]) 124 | 125 | test('Nested Lens type checking', t => [ 126 | t.equal, { a: [1, 2, 3] }>>(), 127 | OK(t)], { a: [1, 2, 3] }>>(), 128 | OK(t), Lens<0>], { a: [1, 2, 3] }>>(), 129 | 130 | t.equal< 131 | Audit, { a: [1, 2, 3] }>, 132 | Lens<["a", 0 | 1 | 2]> 133 | >(), 134 | t.equal< 135 | Audit<[Lens<['a', 'b']>], { a: [1, 2, 3] }>, 136 | [Lens<["a", 0 | 1 | 2]>] 137 | >(), 138 | t.equal< 139 | Audit<[Lens<'a'>, Lens<'b'>], { a: [1, 2, 3] }>, 140 | [Lens<'a'>, Lens<0 | 1 | 2>] 141 | >(), 142 | t.equal< 143 | Audit<['a', Lens<'b'>], { a: [1, 2, 3] }>, 144 | ['a', Lens<0 | 1 | 2>] 145 | >(), 146 | t.equal< 147 | Audit<[Lens<'a'>, 'b'], { a: [1, 2, 3] }>, 148 | [Lens<'a'>, 0 | 1 | 2] 149 | >(), 150 | 151 | // They are expected to flatten 152 | t.equal< 153 | Audit, Lens<'b'>]>, { a: [1, 2, 3] }>, 154 | Lens<['a' , 0 | 1 | 2]> 155 | >(), 156 | ]); 157 | 158 | test('The audit is open-ended when the error is not the last query item', t => [ 159 | t.equal, ['a', ...QueryItem[]]>() 160 | ]) 161 | 162 | test('Successful returns a boolean representing success', t => [ 163 | t.true>>(), 164 | t.false>>(), 165 | ]) -------------------------------------------------------------------------------- /tests/Over.ts: -------------------------------------------------------------------------------- 1 | import { apply, Type, A, B } from 'free-types-core'; 2 | import { test, Context } from 'ts-spec'; 3 | import { Over, $Over, free, Query } from '../src/'; 4 | import { ValidTransform } from '../src/Over'; 5 | import { Next } from '../src/utils'; 6 | 7 | interface $Next extends Type<[number], number> { type: Next> } 8 | 9 | declare const needle: unique symbol; 10 | type needle = typeof needle; 11 | 12 | type Haystack = Map needle) => void, 'bar'] }>; 13 | 14 | 15 | // Over: `any` silences false positive when Query is generic 16 | {type Generic = Over, $Next, any>} 17 | 18 | 19 | test('Over: return the input unchanged when needle is not found', t => [ 20 | t.equal>(), 21 | t.equal>(), 22 | ]) 23 | 24 | const IsStandardCheck = (t: Context) => 25 | t.equal>(); 26 | 27 | test('Over: type checking paths ending with a Type', t => [ 28 | // this passes even though it is not garanteed to run 29 | IsStandardCheck(t)< 30 | ValidTransform<[free.WeakMap], Type<[object, number], [object, unknown]>> 31 | >(), 32 | // check arity 33 | IsStandardCheck(t)< 34 | ValidTransform<[free.WeakMap], Type<[unknown], [object, unknown]>> 35 | >(), 36 | // check return type 37 | IsStandardCheck(t)< 38 | ValidTransform<[free.WeakMap], Type<[object, unknown], number>> 39 | >(), 40 | // check return type 41 | IsStandardCheck(t)< 42 | ValidTransform<[free.WeakMap], Type<[object, unknown], [number]>> 43 | >(), 44 | // check relatedness of arguments 45 | t.equal< 46 | ValidTransform<[free.WeakMap], Type<[string, unknown], [object, unknown]>>, 47 | Type<[[string, 'and', object, 'are unrelated' ], unknown]> 48 | >(), 49 | ]) 50 | 51 | { 52 | type Generic = Over< 53 | [free.WeakMap], 54 | Haystack, 55 | // This transform may succeed or fail depending on the Haystack 56 | // but we can't check it without making Over impractical to use 57 | Type<[object, number], [object, unknown]> 58 | // ------ 59 | >; 60 | } 61 | 62 | test('Over: type checking paths ending with Type arguments', t => [ 63 | 64 | // check arity 65 | t.equal< 66 | ValidTransform<[free.WeakMap, 1], Type<[object, number], number>>, 67 | Type<1, unknown> 68 | >(), 69 | // check Query 70 | t.equal< 71 | ValidTransform<[free.WeakMap, 5], Type<[object, number], number>>, 72 | // because WeakMap only has 2 arguments, the return type can only be `undefined` 73 | Type<1, undefined> 74 | >(), 75 | // check return type 76 | t.equal< 77 | ValidTransform<[free.WeakMap, 0], Type<[unknown], number>>, 78 | Type<1, object> 79 | >(), 80 | // check relatedness of arguments 81 | t.equal< 82 | ValidTransform<[free.WeakMap, 0], Type<[string], object>>, 83 | Type<[[string, 'and', object, 'are unrelated' ]]> 84 | >() 85 | ]) 86 | 87 | test('$Over produces a free type expecting Data', t => { 88 | type $Action = $Over<'foo', $Next>; 89 | type Data = { foo: 1 }; 90 | type Result = apply<$Action, [Data]> 91 | 92 | return t.equal() 93 | }) -------------------------------------------------------------------------------- /tests/Readme.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'ts-spec'; 2 | import { Lens, a, r, Get, Replace, Over, FindPaths, free, self, FindReplace, Audit } from '../src/'; 3 | 4 | declare const needle: unique symbol; 5 | type needle = typeof needle; 6 | 7 | type Haystack = Map needle) => void, 'bar'] }>; 8 | 9 | type YihaStack = Map 'Yiha!') => void, 'bar'] }>; 10 | 11 | type TweenStack = Map Promise) => void, 'bar'] }>; 12 | 13 | type FocusNeedle = Lens<[free.Map, 1, 'foo', 0, a, r]>; 14 | 15 | test('readme example', t => [ 16 | t.equal, needle>(), 17 | t.equal, YihaStack>(), 18 | t.equal, TweenStack>(), 19 | t.equal, YihaStack>(), 20 | t.equal< 21 | FindPaths, [ 22 | [free.Map], 23 | [free.Map, 0], 24 | [free.Map, 1], 25 | [free.Map, 1, "foo"], 26 | [free.Map, 1, "foo", 1], 27 | [free.Map, 1, "foo", 0], 28 | [free.Map, 1, "foo", 0, r], 29 | [free.Map, 1, "foo", 0, a], 30 | [free.Map, 1, "foo", 0, a, a], 31 | [free.Map, 1, "foo", 0, a, r] 32 | ]>(), 33 | t.equal< 34 | FindPaths, [ 35 | [free.Map, 1, "foo", 0, r], 36 | [free.Map, 1, "foo", 0, a], 37 | [free.Map, 1, "foo", 0, a, a], 38 | [free.Map, 1, "foo", 0, a, r] 39 | ]>() 40 | ]) 41 | 42 | declare const foo: < 43 | Path extends readonly string[] & Check, 44 | Obj extends object, 45 | Check = Audit 46 | >(path: Path, obj: Obj, _?: Check) => Path; 47 | 48 | // compiles 49 | const bar = < 50 | const Path extends readonly string[] & Check, 51 | Obj extends object, 52 | Check = Audit 53 | >(path: Path, obj: Obj) => 54 | foo(path, obj) 55 | 56 | // compiles 57 | const baz = (path: P, obj: object) => 58 | foo(path, obj, null as any) 59 | 60 | test('type information is not lost', t => [ 61 | t.equal(bar(['a'], { a: 42 }), ['a'] as const), 62 | t.equal(baz(['a'], { a: 42 }), ['a'] as const) 63 | ]) -------------------------------------------------------------------------------- /tests/Replace.ts: -------------------------------------------------------------------------------- 1 | import { apply } from 'free-types-core'; 2 | import { test } from 'ts-spec'; 3 | import { a, r, Replace, $Replace, free, Query } from '../src/'; 4 | 5 | declare const needle: unique symbol; 6 | type needle = typeof needle; 7 | 8 | type Haystack = Map needle) => void, 'bar'] }>; 9 | 10 | // @ts-expect-error: Check replace value 11 | { type Fail = Replace<[free.WeakSet, 0], WeakSet<{ a: number}>, 3> } 12 | 13 | // @ts-expect-error: Check replace value 14 | { type Fail = Replace<[free.WeakSet], WeakSet<{ a: number}>, [3]> } 15 | 16 | // Replace: `any` silences false positive when Query is generic 17 | {type Generic = Replace, object, any>} 18 | 19 | 20 | test('Replace: return the input unchanged when needle is not found', t => [ 21 | t.equal>(), 22 | t.equal>(), 23 | // @ts-expect-error: [free.Map, 5] is undefined 24 | t.equal>(), 25 | t.equal>(), 26 | t.equal>(), 27 | ]) 28 | 29 | test('Replace: return the input unchanged when the query is `never`', t => [ 30 | t.equal>() 31 | ]) 32 | 33 | test('Bare Replace object', t => [ 34 | t.equal, {a: 'foo', b: 2 }>() 35 | ]) 36 | 37 | test('Bare Replace function', t => [ 38 | t.equal string, string>, (a: string) => string>(), 39 | t.equal string, number>, (a: number) => number>() 40 | ]) 41 | 42 | 43 | test('Bare Replace free type', t => [ 44 | t.equal< 45 | Replace, [string, number]>, 46 | Map 47 | >() 48 | ]) 49 | 50 | test('Replace tuple', t => [ 51 | t.equal, ['foo', 2, 3]>(), 52 | t.equal< 53 | Replace<[1, 2], [1, [2, 20, 200, 2000], 3], 'foo'>, 54 | [1, [2, 20, 'foo', 2000], 3] 55 | >() 56 | ]) 57 | 58 | test('Replace free deep', t => [ 59 | t.equal< 60 | Replace<[1, free.Map, 1, free.Set], [1, Map>, 3], ['foo']>, 61 | [1, Map>, 3] 62 | >() 63 | ]) 64 | 65 | test('$Replace produces a free type expecting Data', t => { 66 | type $Action = $Replace<'foo', 'hello'>; 67 | type Data = { foo: needle }; 68 | type Result = apply<$Action, [Data]> 69 | 70 | return t.equal() 71 | }) -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "noEmitOnError": true, 5 | "noErrorTruncation": true, 6 | "moduleResolution": "node", 7 | "module": "ES2020", 8 | "target": "ESNext", 9 | "strictFunctionTypes": true, 10 | "noImplicitAny": true, 11 | "strict": true 12 | } 13 | } -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "declarationDir": "./dist", 8 | "removeComments": false 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": [] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig-base.json", 3 | "include": [ 4 | "./**/*.ts" 5 | ] 6 | } --------------------------------------------------------------------------------