Idea for Nominal and Structural Types
alias Named = { name: string } | |
// same as `type City = { name: string }` | |
// the structural type on the rhs will get "tagged" by the name on the lhs so | |
// that the original structural type becomes a nominal type | |
// | |
type City = Named | |
type State = Named | |
// don't need to specify type on parameter since anonymous structural type is | |
// inferred by type inference | |
// | |
// fn :: { name: string } -> { name: string } | |
fn makeFancy(x) { | |
x.named += "!" | |
return x | |
} | |
// specifying `Named` acts as just an alias for its anonymous structural type | |
// it's effectively the same as the `makeFancy()` function in terms of type | |
// constraints | |
// | |
// fn :: Named -> Named | |
// fn :: { name: string } -> { name: string } | |
fn makeFancyNamed(x: Named) { | |
x.named += "!" | |
return x | |
} | |
// if I want to require that only a `City` is passed, I need to explicitly | |
// specify a nominal type "tags along" with the original | |
// | |
// fn :: City -> City | |
// fn :: City { name: string } -> City { name: string } | |
fn makeFancyCity(city: City) { | |
city.named += "!" | |
return x | |
} | |
// same reasoning as previous function | |
// | |
// fn :: State -> State | |
// fn :: State { name: string } -> State { name: string } | |
fn makeFancyState(state: State) { | |
state.named += "!" | |
return x | |
} | |
// city :: City { name: string } | |
let city = City { name = "Rome" } | |
// ok: `{ name: string }` is less specific than `City { name: string }` | |
// ok: { name: string } < City { name: string } | |
let fancyNamed = makeFancyNamed(city) | |
// ok: `City` is the same as `City` | |
// ok: City === City | |
let fancyCity = makeFancyCity(city) | |
// error: | |
// `city` has the required structural type of `{ name: string }` but has a | |
// different name. excepted `State` not `City`. | |
// | |
// `City { name: string }` is incompatible with `State { name: string }` due | |
// to nominal type constrains. | |
// | |
// error: City { name: string } =!= State { name: string } | |
let fancyState = makeFancyState(city) | |
// ok: | |
// Using `~` will convert otherwise structurally equivalent types by inferring | |
// the required type. This can be done explicitly by using `x as T` where | |
// `x` is the original variable be converted to the type `T` | |
// | |
// In this scenario, it makes the variable "change names" to the required name | |
// needed to pass `city` of type `City { named: string }` to a parameter | |
// requiring the type `State { named: string }`. | |
// | |
// This works like a "tradition" type cast except that is guaranteed | |
// to be safe since you're the type must be structurally equivalent. | |
// | |
// Likewise, using `~` or `as` will *only* work if the original type can be | |
// safely "casted" to the required type. You'll get a compiler error if the | |
// original type is not structurally equivalent with the required type or the | |
// type "cast" would otherwise fail to satisfy other type constraints | |
// (generics, required operations on the type, etc). | |
// | |
// city :: City { name: string } | |
// ~city :: State { named: string } (inferred) | |
// city as State :: State { named: string } | |
// | |
// ok: ~(City { name: string }) === State { name: string } | |
// ok: City { name: string } as State === State { name: string } | |
let fancyState: State = makeFancyState(~city) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment