In other languages, this would throw a type error - I’m assuming in Motoko this has something in part to do with upgrade compatibility, as mentioned in the type record documentation?
Is there any way that I can ensure the record type being passed in is exact at compile type?
This is simply (the very definition of) subtyping, and as such intentional. In particular, this so-called width subtyping on records is the central mechanism underlying object-oriented languages: a subclass can have more fields than a superclass, but still usable everywhere its superclass is.
In more traditional languages, subtyping isn’t automatic but has to be declared, but the principle is the same. Motoko has structural subtyping, so the subtype relation is inferred. It shares this property with some other more modern languages – TypeScript would be the most well-known example (though unlike TS, Motoko’s type system is sound :)).
Does this then allow a developer to declare an “abstract” supertype record, and extend multiple subtypes of that supertype, such that one could type perform type narrowing/inference on the record passed in? Or for this use case is it more recommended to take the variant & pattern matching approach?
Is there a way to strictly define an object or record type?
I’m not sure I understand the question. Can you give an example? In principle, any record type can serve as an “abstract” supertype, and can have as many subtypes as you want.
You mean, rule out subtyping? No. Why would you need this?
In building out a library, I thought it might be nice to strictly limit the record type that the developer passes in to prevent them from adding in key-value pairs that serve no purpose (dead code), especially if a specific key gets deprecated what the library gets a version bump. It’s not a big deal though, and I totally understand this not being supported.
As far as type narrowing, this is a rough idea of what I was getting at
type Shape = {
color: Text;
}
type Circle = {
radius: Nat;
color: Text;
};
type Square = {
sideLength: Nat;
color: Text
};
// bare with the rough-code, as I didn't check this for compliation errors.
// Hopefully the type-narrowing idea comes across?
func renderShape(shape: Shape): Drawing {
// can I type narrow via record deconstruction?
switch(shape) {
// shape gets narrowed to Square
case ({ sideLength; color }) { renderSquare(shape) };
// shape gets narrowed to Circle
case ({ radius; color }) { renderCircle(shape) };
};
};
func renderCircle(circle: Circle): Drawing {
...
};
func renderSquare(square: Square): Drawing {
...
};
I don’t fully understand the intent of your example, but it looks like you are trying something akin to a downcast. That’s not possible in Motoko. Type information forgotten via subtyping can not be recovered. The renderShape function will never be able to see more than type Shape for its argument, no matter what subtype it’s passed.
If you need to switch on multiple cases, then you will have to use a variant type explicitly.