partial.lenses
partial.lenses copied to clipboard
TypeScript and Flow type definitions
Would be nice to have TypeScript and Flow type definitions for partial lenses.
@gcanti s related work:
- https://github.com/gcanti/monocle-ts
- https://github.com/gcanti/ts-static-land
- https://github.com/gcanti/flow-static-land/blob/master/src/PLens.js#L17
- https://github.com/gcanti/flow-static-land
@polytypic FYI ts-static-land (an early attempt) is superseded by https://github.com/gcanti/fp-ts which goal would be to be compatible with both fantasy-land and static-land (hopefully!)
I made a bit of progress on TS typings in my fork. Not that I imagine typing FP libs to be easy (I ended up as the maintainer of the Ramda typings, so know some of the challenges involved, some yet unresolved), and admittedly I'm still not as fluent in this library yet either. Then again, working on typings will hopefully help there. :)
I'm getting the impression the main challenge here is just... TypeScript's type language can't handle the reduce type logic needed to type R.path
, which means that typing this library for proper type inference becomes... problematic, since that kind of logic is the essential cornerstone here.
So monocle.ts has managed to keep things typed, but essentially at the cost of sacrificing expressivity for verbosity.
This makes me wonder: is there any way we could have our pie and eat it too, in any language (probably meaning Haskell/Purescript, if any at all)? I glanced over their lens libraries listed in the readme here for a bit, but I'm not so familiar with them, and haven't quite managed to tell if any enables expressive constructs as in this library while also being typed without getting a bunch more verbose.
Probably a bit off-topic here, but I'd be pretty curious!
@tycho01 the first example in the README of monocle-ts can be reduced from
import { Lens, Optional } from 'monocle-ts'
const company = Lens.fromProp<Employee, 'company'>('company')
const address = Lens.fromProp<Company, 'address'>('address')
const street = Lens.fromProp<Address, 'street'>('street')
const name = Lens.fromProp<Street, 'name'>('name')
const lens = company
.compose(address)
.compose(street)
.compose(name)
to something like this
import { Lens, Optional } from 'monocle-ts'
const lens2 = Lens.fromPath<Employee, 'company', 'address', 'street', 'name'>(['company', 'address', 'street', 'name'])
where
// more overloadings here...
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(path: [K1, K2, K3, K4]): Lens<T, T[K1][K2][K3][K4]>
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(path: [K1, K2, K3]): Lens<T, T[K1][K2][K3]>
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1]>(path: [K1, K2]): Lens<T, T[K1][K2]>
function fromPath<T, K1 extends keyof T>(path: [K1]): Lens<T, T[K1]>
function fromPath(path: Array<any>) {
const lens = Lens.fromProp<any, any>(path[0])
return path.slice(1).reduce((acc, prop) => acc.compose(Lens.fromProp<any, any>(prop)), lens)
}
Perhaps the same technique can be used to add typings to R.path
and / or this library?
If sticking with strings as well (navigating structures of objects, no arrays), that works. When trying to extend this to numbers to navigate structures including tuples though, this breaks down.
// string, ok
declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let a = path(['a'], { a: 1 }) // number
// number, fails
declare function path<T, K1 extends number>(path: [K1], v: T): T[K1] // Type 'K1' cannot be used to index type 'T'.
let b = path([0], [1])
The R.path
type has indeed used overloading to achieve a type signature that fell short of handling tuples, but for a path length n
, this meant 2^n
extra overloads, and predictably, at a certain number of overloads this just trashed performance to the point applications would no longer compile due to the addition of this type definition. And that's why I'm frustrated with TS for not dealing with type inference in a better (reduce
-enabled) way, and am willing to turn to other languages if needed.
That said, for R.path
forcing users to manually type results could be a solution (expected in/out types), but this sorta means the type definitions are failing their inference job.
From a quick glance of the current library, the situation would get more complex than just string vs. number though: arrays here could contain any optic (to be created using any of the dozens of functions in this lib or just string
/ number
), and essentially you'd end up having to manually type most of it anyway... hence I'm now wondering if there's some Haskell version of this that could get inference right, or if Haskell and JS really just have their own respective strengths.
fell short of handling tuples
FWIW you can see a tuple [A, B]
as an object with '0'
and '1'
props though, so no need for K1 extends number
type A = {
a: [number, [string, boolean]]
}
const lens3 = fromPath<A, 'a', '1', '0'>(['a', '1', '0'])
console.log(lens3.get({ a: [1, ['s', true]] })) // => 's'
AFAIK all solutions in typed languages are based on
- verbosity
- macros (Scala + monocle)
- code generation (PureScript + purescript-derive-lenses)
I did try that idea with inference, though that seemed to have turned out less well somehow:
declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let b = path(['0'], [1])
// intended result: 1
// actual result: error
// Argument of type '["0"]' is not assignable to parameter of type '["length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shi...'.
// Type '"0"' is not assignable to type '"length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shif...'.
That last example on the Monocle site looks pretty good. Just realized much of the folds from this library are also in ekmett/lens, among other things... I feel like this now.
I guess [1]
is inferred as Array<number>
instead of [number]
, this works
declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let b = path(['0'], [1] as [number])
Cool, thanks!
Trying path length 2:
declare function path<T, K1 extends keyof T, K2 extends keyof T[K1]>(path: [K1, K2], v: T): T[K1][K2]
let b2 = path(['a', '0'], { a: [1] })
// Argument of type '["a", string]' is not assignable to parameter of type '["a", never]'.
// Type 'string' is not assignable to type 'never'.
let b3 = path(['a', '0'], { a: [1] } as { a: [number] })
// Argument of type '["a", string]' is not assignable to parameter of type '["a", never]'.
// Type 'string' is not assignable to type 'never'.
Okay TypeScript, I don't think we were meant to work out...
I made a proposal that'd enable the reduce
-like logic needed for typing functions from this repo (as well as similar Ramda ones); currently not looking positive though...
I've recently been exploring type recursion as a way to better address cases like R.path
, which would also solve the basis for the current repo, see here.
Typing this repo with working inference is still my main challenge for TS, a step up still from Ramda. That said, I think we only need two TS upgrades before we could do just about anything...
@tycho01 Really interesting stuff, is there any new news about typings?
@siassaj TS can operate on anything other than generic function types now, given Concat
and pattern matching using conditional types. so while R.path
may have progressed a bit, for this library I'm still not sure if there's much hope yet...
thank you so much for the instant response.
It's a shame, I've run into some typing limitations in my own work too... the really higher order stuff gets so complex so fast...
I'm going to inspect your fork and see if I can't brute force some typings as needed to get most of the safety I need.
@siassaj if you come up with something nice let us know! :dagger:
I have taken a look at monocle-ts
as well as shades
and nothing comes close to this library. Has anyone found a good, typed alternative to partial.lenses
?
Have you looked at optics-ts? It is a new optics library being developed specifically for TypeScript by @akheron.
As another possibility, I recently came up with a new (to me) approach to optics while working in a project developed in C# and made a public repo in F# to demonstrate the approach: NetOptics. The significance of the approach is that it doesn't require an encoding of higher-kinded types, ad hoc polymorphism, or even the use of interfaces to manipulate (higher-kinded) abstractions and yields a relatively concise and efficient implementation. Of course, the approach has its limitations (no (higher-kinded) applicative traversal), but if one is using a language (like TypeScript) that doesn't support higher-kinded types, then the more limited and simpler typings might be a good trade-off (C# 7.3, in particular, doesn't even provide parameterized type aliases, so dealing with complex parameterized types in C# would just be incredibly cumbersome). I have no plans to make a TypeScript library using the approach, but if someone is interested in giving it a try, I can probably help to get started.
Thank you for the reply @polytypic! I'm going to check out if optics-ts
is a good enough substitute and then might approach you for the latter idea if needed. Cheers!
optics-ts
is still pretty new, but the type construction it uses seems to allow typing many optics that other TypeScript libraries cannot type properly. Don't hesitate to create issues or pull requests for missing functionality!