[ANN] Enhanced Ergonomics for Record Types

Hi everyone!

We just published another blog post in our series covering the new features and capabilities coming in ReScript v11. This time it’s about how we’ve added type spreads and coercion to records, to make them more ergonomic to work with and let them get some of the conveniences of structural objects.

Check out the blog post and let us know what you think!

20 Likes

Really cool and very useful language feature! We have a lot of boilerplate code that could be avoided with this!

Will coercion change the shape of the object at runtime? Or are we just changing the type?

This sentence was not clear to me: “Please note: As for right now it is not possible to override fields in the target record type.”

1 Like

This sentence was not clear to me: “Please note: As for right now it is not possible to override fields in the target record type.”

I believe that means you can’t do this:

type x = {
  a: int,
  b: int,
}

type y = {
  ...x,
  b: float,
}
1 Like

Coercion does not change the runtime shape, it’s just at the type level.

@XiNiHa is correct for the second question - there can’t be multiple fields with the same name present when spreading.

Ah ok! Seems like a reasonable constraint. Perhaps the example would clarify this in the blog post!

I am not sure the impact, but wouldn’t that have consequences on the guarantees of runtime representation of records? (eg. you are not sure there are no extraneous fields when receiving a record).

Especially with interop with JS, in this new model there is no guarantee that there are some extra keys in the object that get’s passed to JS, and no way to enforce this at the type level anymore.

No that’s not what it means.
But it’s legit to expect that’s the meaning given how it’s phrased.
You can’t even add back the same field with the same type.

1 Like

Hey,
right now, it’s not possible to spread generic type definitions. Will it be possible at some point?

type x<'a> = {
  a: 'a,
  b: int,
}

type y<'a> = {
  ...x<'a>,
  c: float,
}
1 Like

Added an issue for it: Support type parameters in record type spread · Issue #6291 · rescript-lang/rescript-compiler · GitHub.

2 Likes

Typescript allows to do both

  • Extend and existing types with type intersection,
  • Shrink a type to a field subset using type utility Pick<type,keys>

@zth as you are proposing using spread syntax on types, would it be also possible to destructure a type to shrink it?

I’m not sure I follow what you’re after. Could you show an example of what you mean perhaps?

Consider the following, building 3 new types from an existing one in typescript:

// Base type 
type CatBase = {
    name: string;
    age: number;
    food: 'fish' | 'milk'
}

/**
 * Adds an `id` field to the `Cat` type
 * resulting @type {{
 *  id: number;
 *  name: string;
 *  age: number;
 *  food: 'fish' | 'milk'
 * }}
 */
type CatWithId = CatBase & {
    id: number;
}

/** 
 * Removes the `food` field from the `Cat` type
 * resulting @type {{ name: string; age: number; }}
 */
type CatWithoutFood = Omit<CatBase, 'food'>;


/**
 * Picks only `name` and `food` fields from the `Cat` type
 * resulting @type {{ name: string;  food: 'fish' | 'milk' }}
 */
type CatNameAndFood = Pick<CatBase, 'name' | 'food'>;

The type spread proposal currently allow to build a CatWithId type from CatBase type, however the type spread allows only to extend a type, for shrinking or reducing a type and build CatWithoutFood type and CatNameAndFoodOnly type from CatBase type, you need the inverse operation of spread which would be type destructure.

I’m not really in favor of trying to copy the jungle of features of TS to be honest.

11 Likes

My suggestion would be to avoid thinking that the argument is

if typescript has x, then rescript should too

the core question i’m bringing out is, if proposal allows extending types, is it worth doing the opposite? typescript is not required to say no.

2 Likes

What would be your ideal syntax for a Pick<> equivalent in ReScript?

hope to provide some answers as interesting as your question, can we come out with something idiomatic? here are some ideas:

type catFood =
  | Milk
  | Fish

type rec catBase = {
  name: string,
  age: int,
  food: catFood,
  friends: array<catBase>,
}

// named type destructure, similar to rescript list destructure
type catWithoutFood = catBase{ name, age, friends }

if all you want to check is that catWithoutFood is a subtype of catBase, you can actually already check it like that, I’m pretty sure it would be erased from the generated output anyway:

type catFood =
  | Milk
  | Fish

type rec catBase = {
  name: string,
  age: int,
  food: catFood,
  friends: array<catBase>,
}

// named type destructure, similar to rescript list destructure
type catWithoutFood = {name: string, age: int, friends: array<catBase>}

let _ = ({name: "foo", age: 0, friends: [], food: Milk} :> catWithoutFood)

the idea is to generate new types from existing ones, so avoiding typing all the fields again when you have an existing type template, extending/shrinking a type can be useful when working with 3rd party types.

Do you have a lot of code in production that will be simplified by this feature?

1 Like

The idea of referring to the type of a particular record label seems useful to me.

E.g. in GQL, if you have a record label user.id, and you pass that thing around to leaf components, you don’t necessarily want to duplicate the type definition, but rather piggyback off the GQL type.

In TS, I’d do this:

function UserCard(props: {
  userName: Pick<GQL.User, "name">
}) {

}

It’s not exactly what the OP asked for, but it kinda hits in the same curb, I think.