-
-
Notifications
You must be signed in to change notification settings - Fork 503
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
Add object.prop function #1454
Add object.prop function #1454
Conversation
Thanks @cdimitroulas, two questions:
import { flow, pipe } from 'fp-ts/function'
export declare const prop: <Path extends string>(path: Path) => <Value, Rest>(obj: Record<Path, Value> & Rest) => Value
interface Person {
readonly name: string
readonly age: number
}
declare const p: Person
declare const f: () => Person
// use case 1
const g = flow(f, prop('')) // <= no suggestion, it would be nice to get "name" and "age" as suggestions
// use case 3
export const name = pipe(p, prop('')) // <= idem |
The For example, if we pass the object directly instead of explicitly defining it as the prop('name')({ name: "hello", age: 62 })
// Argument of type '{ name: string; age: number; }' is not assignable to parameter of type
// 'Record<"name", string>'.
// Object literal may only specify known properties, and 'age' does not exist in type
// 'Record<"name", string>'. I'll play around with it to see if we can get autocompletion for the key argument to improve the DX, though I'm not entirely sure how to achieve it |
I tried a few variations of the type signature to see whether I could improve the DX and get suggestions for the key to be passed to |
Ignoring the uncurried side for the minute: const prop = <Obj, Path extends keyof Obj>(path: Path) => (obj: Obj): Obj[Path] => obj[path]; seems close, as autocompletion on the path exists. It does mean having to duplicate the property name when not actually passing in an object, unfortunately: const getStatusText = prop<AxiosResponse, 'statusText'>('statusText'); |
Thanks for looking into it @thewilkybarkid It seems like we need to make a tradeoff here. Your solution helps to autocomplete the path (though when I tried it, it doesn't list all the possible keys for me when I open the quotes, but it does autocomplete when I start typing e.g. when I type "n" it autocompletes "name" for me). On the other hand it makes it more awkward to reuse the partially applied function for different objects because the For example: const prop = <Obj, Path extends keyof Obj>(path: Path) => (obj: Obj): Obj[Path] => obj[path];
interface Person {
readonly name: string
readonly age: number
}
const getName = prop<Person, "name">("name")
// This no longer works because it requires me to pass in a `Person`
getName({ name: "hello" }) |
Until the type system improves might we best solve this with two different functions?: // not checked yet, I'm saying accept anything with a 'k' property
prop('k')
// checked immediately against the provided generic
propFrom<T>()('k') |
You could make the two object types separate: const prop = <Obj, Path extends keyof Obj>(path: Path) => <RealObj extends Record<Path, unknown>>(obj: RealObj): RealObj[Path] => obj[path]; This means that the type of the property can be different. If not desired, that type can be reused: const prop = <Obj, Path extends keyof Obj>(path: Path) => <Value extends Obj[Path], RealObj extends Record<Path, Value>>(obj: RealObj): Value => obj[path]; But not sure if you should be able to pass in a partial object anyhow. (Sounds like the original generic object should be smaller.) |
I'm not sure this is possible in typescript (source): const a: <A extends Record<'a', unknown>>(a: A) => A['a'] = prop('a') The current implementation yields this, which seems less useful: const a: (a: Record<'a', unknown>) => unknown = prop('a') Could something like this work? (Edit: quote added) |
Sorry, meant need to combine with #1454 (comment). Think this covers all the cases: declare function prop<Obj, Path extends keyof Obj>(path: Path): {
<RealObj extends Record<Path, unknown>>(obj: RealObj): RealObj[Path]
(obj: Obj): Obj[Path]
}
declare function prop<Path extends string>(path: Path): <Obj extends Record<Path, unknown>>(obj: Obj) => Obj[Path]
declare function propW<Obj, Path extends keyof Obj>(path: Path): {
<RealObj extends Record<Path, unknown>>(obj: RealObj): RealObj[Path]
}
declare function propW<Path extends string>(path: Path): <Obj extends Record<Path, unknown>>(obj: Obj) => Obj[Path] Edit: just noticed the overloading loses the name autocompletion... Edit 2: This removes overloading, restoring autocompletion but means having to add explicit generics for the reusable functions. |
@anthonyjoeseph I personally find it weird that it infers as @thewilkybarkid what's |
Here's the last playground for tonight. Managed to keep autocompletion working, but allow for either no generics or only an object type (so no need to also pass in the property name too). Since it works without any generics at all, |
Nice! The only issue I see with that is that we default to For example, this compiles even though the key does not exist in const getPersonName = prop<Person>('banana') |
OK I was definitely wrong about the partial application thing - my mistake. Taking inspiration from @thewilkybarkid 's excellent work, I think I found a solution that |
Nice one, this looks quite good in terms of how well it works with the various use-cases. It's a bit scary that we need to use two assertions to |
That can be solved with a couple less scary assertions |
Perfect! This works great. I'll update the PR with this new implementation. Are we happy to drop the uncurried overload? Personally I'm ok with it |
I've just had a play and looks like the best option. 👍 |
b6b0b3d
to
0ea8479
Compare
@gcanti for some reason I got a bunch of unrelated docs changes after I rebased my branch and I reran |
Turned my playground into a dtslint file (cdimitroulas#1). |
@cdimitroulas ah sorry, I'm not generating the docs on every commit as it slowed me down. I was thinking about the usefulness of a This use case is not compelling interface Person {
readonly name: string
readonly age: number
}
declare const p: Person
pipe(p, prop('name')) as you can just project with the same effort (and standard autocompletion) pipe(p, (_) => _.name) If I want a generic "accept anything with a 'name' property", I would expect a polymorphic function as a result: // const getName: <A>(obj: Record<"name", A>) => A
const getName = prop('name') Another use case that would be nice is to make import * as S from 'fp-ts/string'
import * as Ord from 'fp-ts/Ord'
import * as Eq from 'fp-ts/Eq'
interface Person {
readonly name: string
readonly age: number
}
declare const p: Person
const eq = pipe(
S.Eq,
Eq.contramap((p: Person) => p.name) // <= awkward
)
// const myEq: Eq.Eq<Record<"name", string>>
const myEq = pipe(S.Eq, Eq.contramap(prop('name'))) // nice
// const myOrd: Ord.Ord<Record<"name", string>>
const myOrd = pipe(S.Ord, Ord.contramap(prop('name'))) // nice Other use cases? |
I can understand your argument, but I think many in the ecosystem find this to be slightly awkward as well, and would prefer to do it with a function like The contramap use-case is nice 👍 |
That's the current behavior - would you like to remove the autocomplete functionality to simplify it? |
365bef9
to
8ae774f
Compare
I tested out the // const myEq: Eq.Eq<Record<"name", string>>
const myEq = pipe(Eq.eqString, Eq.contramap<string, Person>(prop('name'))) // nice
// const myOrd: Ord.Ord<Record<"name", string>>
const myOrd = pipe(Ord.ordString, Ord.contramap<string, Person>(prop('name'))) // nice So it doesn't seem like the |
Good catch @cdimitroulas ! I think I've solved this (source) - please forgive my Edit: updated to remove the |
Hey there, thanks for the work and for the proposal! I'm concerned about the boundaries of
|
@raveclassic there's a lot of discussion over at #1460 relating to similar |
@raveclassic very good points, definitely important stuff to consider. I know this wasn't your point, but you made me realize that Edit: updated to remove the |
Slightly unrelated but @raveclassic I've got this document that attempts to keep track of Ramda equivalency in fp-ts[-std], it might prove useful. |
@cdimitroulas would you mind updating this PR to be against |
Done @anthonyjoeseph |
I've published a library called spectacles-ts that mirrors this functionality (and is a facade for most of monocle-ts) - it'd be great to get feedback from all you smart folks on this thread! import { get } from 'spectacles-ts'
import * as Eq from 'fp-ts/Eq'
import { Eq as StringEq } from 'fp-ts/string'
const piped: number = pipe({ a: { b: 123 } }, get('a', 'b'))
const polymorphic: number = get('a', 'b')({ a: { b: 123 } })
const inferred: Eq.Eq<{ a: { b: string } }> = pipe(
StiringEq,
Eq.contramap(get('a', 'b'))
) |
New feature for
object
module:prop
functionReasoning: Small and handy utility function which is also something that many folks in the JS ecosystem are used to and learn when getting started with functional programming. Usually they use something like
get
from lodash or they see theprop
function from the Mostly Adequate Guide to FP (https://mostly-adequate.gitbook.io/mostly-adequate-guide/appendix_c#prop).Question: Do you prefer to add overloads so that we can have both curried/uncurried versions of this? (e.g.
prop('myKey', myObject)
orprop('myKey')(myObject)
)