How can get keys of object in generic

For example I can use keyof in TS:

type User = {
  name: string
  age: number
}

type UserFields = keyof User // "name" | "age"

How can I get same behavior?

I need it for make a function what will read property of object in pipeline.
Example:

type user = {
  name: string
  age: int
}

let prop = (obj: 't, key: keyof 't) => obj[key]

...

getUser()->prop("age")-> ...

Hi @snatvb would not getUser().age fit your requirement?

ReScript does not support this kind of typing. If you can describe what you are trying to solve though, there is usually another way to do it.

Could show me an example with my case?

I think that ReScript should support this possibility

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.

For example I want iterate by all keys in structure – I need keyof
How can I get new type without some property?

type User = {
  name: string
  age: number
}

type Foo = Omit<User, 'name'>

If I need to do with some field I can use this:

type OnlyNumber<T extends {}, K extends keyof T> = T[K] extends number ? K : never;

const addToProp = <T extends {}, K extends keyof T>(obj: T, key: OnlyNumber<T, K>, n: number) => {
    const oldValue = obj[key] as unknown as number
    return {
        ...obj,
        [key]: oldValue + n
    }
}



type User = {
  name: string
  age: number
  some: {}
}

const user: User = {
    name: 'Jon',
    age: 23,
    some: {}
}

addToProp(user, 'age', 3)

TS Playground

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.

What about first example?

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)
1 Like

Hey @kevanstannard, thanks you again!
But I think that need to add possibility for work with keys for interop with TS.

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.

1 Like

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:

let age = (user: user) => user.age

getUser()->age

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()
}

TS Playgroud

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
}

(Playground)

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 :slight_smile:

EDIT: wording, grammar.

9 Likes

WoW! This looks great! Thanks a lot!

I will check your decision.
Maybe User.t and User.field could link together with using Polymorphic Variant as keys.

I checked your decision and I can say that it’s ok for me right now. I marked your comment as solution. Thanks again. :slight_smile:

2 Likes

for the sake of exhaustiveness, there are other solutions that don’t involve functors like this one:

type firestore

type collection<'item, 'orderBy>

module Collection = {
  @send
  external orderBy: (
    collection<'item, 'orderBy>,
    'orderBy,
  ) => collection<'item, 'orderBy> = "orderBy"
  @send external asList: collection<'item, _> => array<'item> = "asList"
}

module User = {
  type t = {name: string, age: int}
  type collection = collection<t, [#name | #age]>
  @send
  external getUsers: (firestore, @as("users") _) => collection = "collections"
  include Collection
}

let doSome = (firestore: firestore): array<User.t> => {
  open User
  firestore->getUsers->orderBy(#name)->asList
}

Playground

6 Likes

I think that solution with functors is better. Thanks anyway :slight_smile: