Skip to content

Commit

Permalink
feat: add type checking to Lens (flat paths) — WIP #1
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffreytools committed Aug 5, 2023
1 parent 91ba908 commit 9759b2a
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 26 deletions.
81 changes: 81 additions & 0 deletions src/Audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { TypesMap, Generic, unwrap } from 'free-types-core';
import { Fn, Param, Output } from './types'
import { Prev, Next } from './utils';
import { Lens } from './Lens'
import { FollowPath, NOT_FOUND } from './Follow';

export type Audit<
L extends Lens,
Model,
I extends number = 0,
F = FollowPath<L['path'][I], Model, Model>
> = F extends NOT_FOUND ? [...LastPathItem<L, I>, NextPathItem<Model>]
: Next<I> extends L['path']['length'] ? F
: Audit<L, F, Next<I>>;

type LastPathItem<L extends Lens, I extends number> =
I extends 0 ? [] : [L['path'][Prev<I>]];

type NextPathItem<Model> =
readonly any[] extends Model ? number
: Model extends readonly unknown[] ? NumericArgs<Model>
: Model extends Fn ? Output | Param<SequenceTo<Prev<Parameters<Model>['length']>>>
: Model extends GenericFree ? TypesMap[unwrap<Model>['URI']]
: Model extends Record<PropertyKey, unknown> ? { [K in keyof Model]: K }[keyof Model]
: never

type NumericArgs<T> = ToNumber<keyof T & `${number}`>

type ToNumber<T> = T extends `${infer I extends number}` ? I : never

type GenericFree = Exclude<
Generic<TypesMap[keyof TypesMap]>,
Fn | readonly unknown[] | Record<PropertyKey, unknown>
>;

type SequenceTo<N extends number, I extends number = 0, R = never> =
I extends N ? R | I
: SequenceTo<N, Next<I>, R | I>

type Parameters<F, P =
F extends {
(...args: infer A): any
(...args: infer B): any
(...args: infer C): any
(...args: infer D): any
(...args: infer E): any
(...args: infer F): any
} ? A | B | C | D | E | F

: F extends {
(...args: infer A): any
(...args: infer B): any
(...args: infer C): any
(...args: infer D): any
(...args: infer E): any
} ? A | B | C | D | E

: F extends {
(...args: infer A): any
(...args: infer B): any
(...args: infer C): any
(...args: infer D): any
} ? A | B | C | D

: F extends {
(...args: infer A): any
(...args: infer B): any
(...args: infer C): any
} ? A | B | C

: F extends {
(...args: infer A): any
(...args: infer B): any
} ? A | B

: F extends {
(...args: infer A): any
} ? A

: never
> = P extends any ? unknown[] extends P ? never : P : never;
20 changes: 13 additions & 7 deletions src/Lens.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { Type } from 'free-types-core';
import { PathItem, Query } from './types';
import { Next } from './utils';

import { PathItem, Query } from './types'
import { Type, A } from 'free-types-core';
import { Audit } from './Audit';
import { Get } from './Get';

export { Lens, $Lens }

type Lens<Q extends Query=never> =
[Q] extends [never] ? { __type_lenses: 'lens' , path: PathItem[] }
type Lens<Q extends Check & Query = never, Model = never, Check = CheckQuery<Q, Model>> =
[Q] extends [never] ? { __type_lenses: 'lens', path: PathItem[] }
: Q extends { __type_lenses: 'lens' } ? Q
: Q extends PathItem ? { __type_lenses: 'lens', path: [Q] }
: Q extends (PathItem | Lens)[] ? { __type_lenses: 'lens', path: Flatten<Q> }
: never ;

interface $Lens extends Type<[Query], Lens> {
type: this[A] extends Query ? Lens<this[A]> : Lens
type: this[0] extends Query ? Lens<this[0]> : Lens
}

type Flatten<T extends (PathItem | Lens)[], I extends number = 0, R extends PathItem[] = []> =
Expand All @@ -22,4 +23,9 @@ type Flatten<T extends (PathItem | Lens)[], I extends number = 0, R extends Path
? Flatten<T, Next<I>, [...R, T[I]]>
: T[I] extends Lens
? Flatten<T, Next<I>, [...R, ...T[I]['path']]>
: never;
: never;

type CheckQuery<Q extends Query, Model> =
[Model] extends [never] ? unknown
: [Get<Q, Model>] extends [never] ? Audit<Lens<Q>, Model>
: unknown;
20 changes: 1 addition & 19 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
export { Next, Prev, Last, Assert };

type Prev<I extends number> = _prev[I];
type Next<I extends number> = _next[I];

type Last<T extends unknown[]> = T[Prev<T['length']>]

type _prev = [never, 0,..._next];
type _next = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11,12,13,14,15,16,17,18,19,20,
21,22,23,24,25,26,27,28,29,30,
31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,
51,52,53,54,55,56,57,58,59,60,
61,62,63,64
];

type Assert<T, U> = T extends U ? T : never
export { Next, Prev, Last } from 'free-types-core/dist/utils';
39 changes: 39 additions & 0 deletions tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,45 @@ test('Lens composition' as const, t =>
>()
)

{ 'Flat Lens type checking'

// OK
{ type L = Lens<['a'], { a: [1, 2, 3] }> }
{ type L = Lens<['a', 0], { a: [1, 2, 3] }> }
{ type L = Lens<['a', 'c'], { a: { c: 1 } }> }
{ type L = Lens<[free.Map], Map<string, unknown>> }
{ type L = Lens<['a', free.Map], { a: Map<string, unknown> }> }
{ type L = Lens<[a], (a: any, b: any) => unknown> }

// @ts-expect-error "b" is not assignable to "a"
{ type L = Lens<['b'], { a: [1, 2, 3] }> }

// @ts-expect-error: "b" is not assignable to 0 | 1 | 2
{ type L = Lens<['a', 'b'], { a: [1, 2, 3] }> }

// @ts-expect-error: "b" is not assignable to "c"
{ type L = Lens<['a', 'b'], { a: { c: 1 } }> }

// @ts-expect-error: "b" is not assignable to $Map
{ type L = Lens<['a', 'b'], { a: Map<string, unknown> }> }

// @ts-expect-error: "b" is not assignable to Output | Param
{ type L = Lens<['a', 'b'], { a: (...args: any[]) => unknown }> }

// @ts-expect-error: $Set is not assignable to $Map
{type L = Lens<[free.Set], Map<string, unknown>>}

// @ts-expect-error: [1, $Set] is not assignable to [1, $Map].
{type L = Lens<[1, free.Set], [0, Map<string, unknown>]>}

// @ts-expect-error: [$Map, 2] is not assignable to [$Map, 0 | 1]
{type L = Lens<[free.Map, 2], Map<string, unknown>>}

// @ts-expect-error: b is not assignable to Output | Param<0>
{ type L = Lens<[b], (a: any) => unknown> }

}

test('bare Get: tuple, object' as const, t => [
found(t)<Get<0, [needle, 2, 3]>>(),
found(t)<Get<'a', { a: needle, b: 2 }>>(),
Expand Down

0 comments on commit 9759b2a

Please sign in to comment.