Type Record is not strict - allows subtype to be passed in

If I have the type record

type UserAttributes = {
  name: Text;
  age: Nat;
};

And am using the record type in a function, like

func addUser(user: UserAttributes) { ... };

Then if I pass in a subtype of user, say something like

addUser({
  name = "Joey";
  age = 10;
  city = "Kalamazoo";
});

This will still compile. See the code here → Motoko Playground - DFINITY

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 :)).

4 Likes

Got it, thanks. Found this very helpful as well after reading your comment Local objects and classes | Internet Computer Home

Two follow-up questions:

  1. 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?

  2. Is there a way to strictly define an object or record type?

I’m curious why this is a concern. Could you help me understand that?

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.

1 Like