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.
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.
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.
To me these (add/remove/pick) operations would be better suited to being exposed as a library module like Belt.Array / Belt.Option rather than adding customizations to the language/compiler.
Something like Belt.Record. So the code I imagine would look like:
module Record = Belt.Record
type catFood =
| Milk
| Fish
type catBase = {
name: string,
age: int,
food: catFood
}
type catWithId = catBase->Record.add({id: int})
type catWithoutFood = catBase->Record.omit(["food"])
type catNameAndFood = catBase->Record.pick(["name", "food"])
// More imaginary operations
let doesCatHaveFood = catWithoutFood->Record.find("food") // false
let isCatWithoutFoodSubType = catWithoutFood->Record.compareWithField(catBase, "food") // -1 or 0 or 1
This also means we might need to look at justifying having first class types (which goes against adding features to the language), so we could pass types as arguments to functions.
I want ReScript to be small and simple; but with TS features being looked at, we might need to think through the possible options to keep the language lean and simple.
how do you implement those library functions…they will still need language support internally and youre back in the same place but have also added type level functions?
You are correct and I did mention, it goes against adding features to the language.
My point was simply this: If we have library modules for data structures like Option and Array, it makes sense to follow the same tradition for Record as well.
I realize that the add/remove/pick operations for records are more like pattern matching/destructuring for arrays. In the sense that they operate on the structure of the type rather than operating on the data of the said type.
// for array arr
// this requires language support to destructure
switch arr {
| [] => "empty"
| [one] => "single item"
| [one, _] => "multiple items"
}
// whereas this is a library function
let length = arr->Belt.Array.length
// for record type catBase
// this would require language support
catBase & { id: number }
// whereas for data of type catBase
let neko = {
name: "Neko",
age: 4,
food: Fish
}
// this would be a library function
let fields = neko->Belt.Record.keys
With the current set of language features, this is an important distinction that I missed. Thank you for pointing me to look at what needs language support.
Indeed there are many little features like pick one can come up with. And they all add up easily: more complexity.
If on the other hand there are more core, powerful features that open up a number of different applications, then those are more likely to be adopted.
For example, a more core feature that makes pick and 5 other new things expressible while learning only 1, could be interesting.
It is interesting to note that pick operation is redundant since it can be replicated by omit/remove. Though it can get tedious if you had to pick one out of 10 fields, in which case you have to remove 9 fields or you can create a new type without those 9 fields.
I think the add and remove operations might be sufficient:
type catFood = Milk | Fish
type catBase = {
name: string,
age: int,
food: catFood
}
type catWithId = catBase & {id: int}
type catWithoutFood = catBase ! {food: catFood}
type catNameAndFood = catBase ! {age: int}
// And the ability to chain them:
type person: = {
name: string,
age: int
}
type catWithOutFoodButWithOwner = catBase ! {food: catFood} & {owner: person}
// generates
// type catWithOutFoodButWithOwner = {
// name: string,
// age: int,
// owner: person
// }
Note: I did not illustrate with spread (…) operator because I could not find a good remove syntax.