Skip to content

Commit

Permalink
fix: specify the behaviour of Get/Replace/Over when the needle is not…
Browse files Browse the repository at this point in the history
… found — WIP #1
  • Loading branch information
geoffreytools committed Aug 5, 2023
1 parent 26d0c7e commit 6ce499c
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 25 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ type Needle = Get<FocusNeedle, Haystack>; // needle
## Querying and modifying types

### `Get`
Return the queried piece of type.
Return the queried piece of type or `never` if it is not found.

#### Syntax
`Get<Query, Haystack, Self>`
Expand All @@ -180,7 +180,7 @@ The same as `Get`, but takes a tuple of `Query` and returns a tuple of results.

### `Replace`

Replace the queried piece of type with a new value in the parent type.
Replace the queried piece of type with a new value in the parent type, or return the parent type unchanged if the query failed.

#### Syntax
`Replace<Query, Haystack, Value>`
Expand All @@ -193,7 +193,7 @@ Replace the queried piece of type with a new value in the parent type.

### `Over`

Map over the queried piece of type with a free type
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.

#### Syntax
`Over<Query, Haystack, $Type>`
Expand Down
32 changes: 25 additions & 7 deletions src/Follow.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { self, Param, Output, Key, PathItem, Indexable, Fn } from './types'
import { Type, inferArgs } from 'free-types-core';
import { self, Param, Output, Key, PathItem, Fn } from './types'
import { Type, inferArgs, Generic, apply } from 'free-types-core';

export { FollowPath }
export { FollowPath, NOT_FOUND }

declare const NOT_FOUND: unique symbol;
type NOT_FOUND = typeof NOT_FOUND;

type FollowPath<I extends PathItem, Data, Self> =
I extends Output ? Data extends Fn ? ReturnType<Data> : never
: I extends Param ? Data extends Fn ? Parameters<Data>[I['key']] : never
: I extends Key ? Data extends Indexable ? Data[I] : never
I extends Output ? Data extends Fn ? ReturnType<Data> : NOT_FOUND
: I extends Param ? Data extends Fn ? Parameters<Data>[I['key']] : NOT_FOUND
: I extends Key ?
Data extends ReadonlyArray<unknown>
? I extends keyof Data
? Data[I] extends undefined ? NOT_FOUND : Data[I]
: NOT_FOUND
: Data extends Record<PropertyKey, unknown>
? I extends keyof Data ? Data[I] : NOT_FOUND
: NOT_FOUND
: I extends self ? Self
: I extends Type ? inferArgs<Data, I> : never
: I extends Type ? FreeTypeArgs<Data, I>
: NOT_FOUND

type FreeTypeArgs<T, $T extends Type> =
apply<$T, any[]> extends T
? T extends Generic<$T>
? inferArgs<T, $T>
: NOT_FOUND
: NOT_FOUND;
15 changes: 10 additions & 5 deletions src/Get.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Query } from './types'
import { Next } from './utils';
import { FollowPath } from './Follow';
import { FollowPath, NOT_FOUND } from './Follow';
import { Lens } from './Lens';
import { Type, Checked, Optional, A, B, C, $partial, $apply } from 'free-types-core'
import { MapOver, _$Optional, _ } from 'free-types/essential';
Expand All @@ -10,10 +10,15 @@ export { Get, GetMulti, $Get, $GetMulti }
type Get<Q extends Query, Data, Self = Data> =
_Get<Lens<Q>, Data, Self>

type _Get<L extends Lens, Data, Self, I extends number = 0> =
Next<I> extends L['path']['length']
? FollowPath<L['path'][I], Data, Self>
: _Get<L, FollowPath<L['path'][I], Data, Self>, Self, Next<I>>;
type _Get<
L extends Lens,
Data,
Self,
I extends number = 0,
F = FollowPath<L['path'][I], Data, Self>
> = F extends NOT_FOUND ? never
: Next<I> extends L['path']['length'] ? F
: _Get<L, F, Self, Next<I>>;

// naive implementation but it is not obvious that a custom traversal would perform better
type GetMulti<Qs extends Query[], Data, Self = Data> =
Expand Down
10 changes: 5 additions & 5 deletions src/Over.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PathItem, Query } from './types'
import { Type, apply } from 'free-types-core';
import { Next } from './utils';
import { ModifyPath } from './Modify';
import { FollowPath } from './Follow';
import { FollowPath, NOT_FOUND } from './Follow';
import { Lens } from './Lens';

export { Over, $Over };
Expand All @@ -16,10 +16,10 @@ type _Over<
V extends Type,
I extends number = 0,
C extends PathItem = L['path'][I],
> =
I extends L['path']['length']
? apply<V, [Data]>
: ModifyPath<C, Data, _Over<L, FollowPath<C, Data, Data>, V, Next<I>>>;
F = FollowPath<C, Data, Data>
> = I extends L['path']['length'] ? apply<V, [Data]>
: F extends NOT_FOUND ? Data
: ModifyPath<C, Data, _Over<L, F, V, Next<I>>>;

interface $Over<Q extends Query, V extends Type> extends Type<1> {
type: _Over<Lens<Q>, this[0], V>
Expand Down
9 changes: 6 additions & 3 deletions src/Replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Type } from 'free-types-core';
import { Next, Last } from './utils';
import { ModifyPath } from './Modify';
import { Lens } from './Lens';
import { FollowPath } from './Follow';
import { FollowPath, NOT_FOUND } from './Follow';

export { Replace, $Replace };

Expand All @@ -19,8 +19,11 @@ type _Replace<
V,
I extends number = 0,
C extends PathItem = L['path'][I],
> = I extends L['path']['length'] ? V
: ModifyPath<C, Data, _Replace<L, FollowPath<C, Data, Data>, V, Next<I>>>;
F = FollowPath<C, Data, Data>,
> =
I extends L['path']['length'] ? V
: F extends NOT_FOUND ? Data
: ModifyPath<C, Data, _Replace<L, F, V, Next<I>>>;

interface $Replace<Q extends Query, V extends ValidValue<Q>> extends Type<1> {
type: _Replace<Lens<Q>, this[0], V>
Expand Down
42 changes: 40 additions & 2 deletions tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,46 @@ const found = <T>(t: Context<T>) => t.equal<needle>();
found(t)<Get<FocusNeedle, Haystack>>(),
t.equal<Replace<FocusNeedle, Haystack, 'Yiha!'>, YihaStack>(),
t.equal<Over<FocusNeedle, Haystack, free.Promise>, TweenStack>()
])
}
];
})


test('Get: return `never` when needle is not found' as const, t => {
type Haystack = Map<string, { foo: [(f: (arg: string) => needle) => void, 'bar'] }>;

return [
t.never<Get<[free.Function], Haystack>>(),
t.never<Get<[free.ReadonlySet], Haystack>>(),
t.never<Get<[free.Map, 5], Haystack>>(),
t.never<Get<[free.Map, 1, 'bar'], Haystack>>(),
t.never<Get<[free.Map, 1, 'foo', 2], Haystack>>(),
t.equal<[never], GetMulti<[[free.Function]], Haystack>>(),
];
})

test('Replace: return the input unchanged when needle is not found' as const, t => {
type Haystack = Map<string, { foo: [(f: (arg: string) => needle) => void, 'bar'] }>;

return [
t.equal<Haystack, Replace<[free.Function], Haystack, ['X'[], 'X']>>(),
t.equal<Haystack, Replace<[free.ReadonlySet], Haystack, ['X']>>(),
t.equal<Haystack, Replace<[free.Map, 5], Haystack, 'X'>>(),
t.equal<Haystack, Replace<[free.Map, 1, 'bar'], Haystack, 'X'>>(),
t.equal<Haystack, Replace<[free.Map, 1, 'foo', 2], Haystack, 'X'>>(),
];
})

test('Over: return the input unchanged when needle is not found' as const, t => {
type Haystack = Map<string, { foo: [(f: (arg: string) => needle) => void, 'bar'] }>;

return [
t.equal<Haystack, Over<[free.Function], Haystack, free.Promise>>(),
t.equal<Haystack, Over<[free.ReadonlySet], Haystack, free.Promise>>(),
t.equal<Haystack, Over<[free.Map, 5], Haystack, free.Promise>>(),
t.equal<Haystack, Over<[free.Map, 1, 'bar'], Haystack, free.Promise>>(),
t.equal<Haystack, Over<[free.Map, 1, 'foo', 2], Haystack, free.Promise>>(),
];
})

test('r' as const, t => [
t.equal<r, Output>(),
Expand Down

0 comments on commit 6ce499c

Please sign in to comment.