New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript and Flow type definitions #55

Open
polytypic opened this Issue Feb 10, 2017 · 12 comments

Comments

Projects
None yet
3 participants
@polytypic
Copy link
Member

polytypic commented Feb 10, 2017

Would be nice to have TypeScript and Flow type definitions for partial lenses.

@polytypic polytypic changed the title TypeScript type definitions TypeScript and Flow type definitions Feb 17, 2017

@gcanti

This comment has been minimized.

Copy link

gcanti commented Feb 17, 2017

@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!)

@tycho01

This comment has been minimized.

Copy link

tycho01 commented Mar 15, 2017

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. :)

@tycho01

This comment has been minimized.

Copy link

tycho01 commented Mar 31, 2017

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!

@gcanti

This comment has been minimized.

Copy link

gcanti commented Mar 31, 2017

@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?

@tycho01

This comment has been minimized.

Copy link

tycho01 commented Mar 31, 2017

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.

@gcanti

This comment has been minimized.

Copy link

gcanti commented Mar 31, 2017

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

@tycho01

This comment has been minimized.

Copy link

tycho01 commented Mar 31, 2017

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.

@gcanti

This comment has been minimized.

Copy link

gcanti commented Mar 31, 2017

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])
@tycho01

This comment has been minimized.

Copy link

tycho01 commented Mar 31, 2017

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...

@tycho01

This comment has been minimized.

Copy link

tycho01 commented May 18, 2017

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...

@tycho01

This comment has been minimized.

Copy link

tycho01 commented Jun 26, 2017

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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment