To come up with an alternative, we would need the wider context of what you are trying to do. Otherwise we might give you a solution that is not helpful for you.
A bit more context needed. Where/how would you use this addToProp function? Why would you need this complexity specifically, instead of doing {...user, age: user.age + 3}?
I can’t interop types from TS to ReScript. Because ReScript doesn’t support this a little bit moment.
I show simple example. I can’t to write complexity example right now.
Hi @snatvb, I believe you may not be able to have a generic addToProp() function. You might need to be a bit more specific with your types. For example:
type person = {"name": string, "age": int}
type fossil = {"location": string, "age": int}
type product = {"name": string, "count": int}
let incrementAge = (o: {.."age": int}, amount: int) =>
Js.Obj.empty()->Js.Obj.assign(o)->Js.Obj.assign({"age": o["age"] + amount})
let incrementCount = (o: {.."count": int}, amount: int) =>
Js.Obj.empty()->Js.Obj.assign(o)->Js.Obj.assign({"count": o["count"] + amount})
let person = incrementAge({"name": "Person", "age": 20}, 1)
let fossil = incrementAge({"location": "Location", "age": 100}, 50)
let product = incrementCount({"name": "Product", "count": 0}, 5)
Or with records:
type person = {name: string, age: int}
type fossil = {location: string, age: int}
type product = {name: string, count: int}
let incrementPersonAge = (person: person, amount: int) => {...person, age: person.age + amount}
let incrementFossilAge = (fossil: fossil, amount: int) => {...fossil, age: fossil.age + amount}
let incrementProductCount = (product: product, amount: int) => {
...product,
count: product.count + amount,
}
let person = incrementPersonAge({name: "Person", age: 20}, 1)
let fossil = incrementFossilAge({location: "Location", age: 100}, 50)
let product = incrementProductCount({name: "Product", count: 0}, 5)
Could you give a specific example of TS code you’re trying to interop with, together with the practical problem you’re trying to solve with ReScript while interacting with that code?
Your addToProp example doesn’t look very practical (more like, hey, look what derived types can do). And your first example, I think, is missing any interop in it: it’s just trying to create a generic function that you can totally do without (just use getUser().age, as @a-c-sreedhar-reddy has pointed out).
It’s not that having keyof etc. wouldn’t be nice (for instance, I think that deriving record types from other records types would be very convenient for JSON decoders), but it’ll probably come at a price. The type system is going to be harder to grasp, harder to maintain, will likely have more bugs (defying the purpose of a sound type system), and (don’t quote me on that, but) type checking might become much slower, while ReScript strives to be blazing fast.
This is a bit similar to my first ReScript question here:
The short version is that ReScript is about explicit operations on known data vs. a more dynamic approach which might say arg 1 might have an age property and arg 2 might be a function to get an age property.
In that sense it may be expected to write a function like the following:
Ok. How can I write correct interop with firebase? With correct types.
type User = {
name: string
age: number
}
...
const users = firestore.collection('users').orderBy('name')
collection method receives only pool of strings (by collection name). It gets some type by name and gets keys of this type. Method orderBy can’t to get incorrect field name.
TS example types:
type User = {
name: string
age: number
}
type Collection<T> = {
orderBy: (fieldName: keyof T) => Collection<T>
asList: () => T[]
}
type Collections = {
users: Collection<User>
}
type Firestore = {
collection: <T extends keyof Collections>(collection: T) => Collections[T]
}
const doSome = (firestore: Firestore) => {
const users = firestore.collection('users').orderBy('name')
return users.asList()
}
Well, here’s how I might do it with some opaque types and functors (which, as you might guess by the state of the docs, are not easily recommended for newcomers, being a rather advanced technique, but they can be really useful):
type firestore
module type CollectionItem = {
type t
type field
}
module Collection = (Item: CollectionItem) => {
type t
type item = Item.t
@send external orderBy: (t, Item.field) => t = "orderBy"
@send external asList: t => array<Item.t> = "asList"
}
module User = {
type t = {name: string, age: int}
type field = [#name | #age]
}
module UsersCollection = Collection(User)
module Collections = {
@send
external getUsers: (firestore, @as("users") _) => UsersCollection.t =
"collections"
}
let doSome = (firestore: firestore) => {
open UsersCollection
firestore->Collections.getUsers->orderBy(#name)->asList
}
How do I feel about it compared to TS? First of all, there’s some obvious boilerplate, but I’m not sure it’s worse than its TypeScript counterpart. Second, I’m not too worried about the getUser function: I think it might be ok to write bindings like that for all of your collections (unless you work with your collections in a very dynamic way, in which case I’d model it as a dictionary of some sorts). Actually, you’re encouraged to write bindings like that for your app’s specific needs.
What worries me the most here (and which might be a real case for something like keyof) is the fact that User.t and User.field are completely unrelated, so you can sort on fields that don’t exist in the record. (What placates me a bit is that at least t and field are colocated.)
I still don’t think it means the team absolutely must implement this feature: my hunch is that it’s costly and disruptive. But at least we have a case