orbitjs / orbit

Composable data framework for ambitious web applications.
https://orbitjs.com
MIT License
2.33k stars 133 forks source link

Typed Model Deserialization #490

Open jaredcnance opened 6 years ago

jaredcnance commented 6 years ago

Currently, models are deserialized into Record instances and the attributes are Dict<any> https://github.com/orbitjs/orbit/blob/21aa956ec242dad4f68ba42cc3a1e67ddd6cd94b/packages/%40orbit/data/src/record.ts#L19-L21

Is there a mechanism for deserializing to a stronger type? Something like:

let articles  = await store.query<Article>(q => q.findRecords('article'));

// do stuff against Array<Article>

If not is this something that you'd be in favor of?

dgeb commented 6 years ago

@jaredcnance Sorry to delay with a response. Yes, I'm in favor of stronger typings where possible. For instance Article extends Record in your example. It would be ideal to also be able to use those typings to inform the schema.

Definitely open to suggestions!

makepanic commented 5 years ago

I got some minutes to think about it. I guess the common case is to have attribute types.

Here's a simplified example what could one do to improve types, if a developer provides an interface for model attributes:

// from @orbit/data/**/schema.ts
interface AttributeDefinition {
  type?: string;
  serializationOptions?: Dict<any>;
  deserializationOptions?: Dict<any>;
}

// adjusted ModelDefinition
interface ModelDefinition<A = {[key: string]: any}> {
  keys?: Dict<KeyDefinition>;
  attributes?: { [field in keyof A]: AttributeDefinition };
  relationships?: Dict<RelationshipDefinition>;
}

With these base types, a user can create an interface that represents its model attributes:

// developer defined resource attribute
interface MyAttributes {
  created: Date;
  amount: number;
  name: string;
}

Now we can use that to improve schema types which will automatically require all attribute fields to be defined.

const modelSchema: ModelDefinition<MyAttributes> = {
  attributes: {
    created: {type: 'date'},
    // error because not a `AttributeDefinition`
    amount: 'number',
    // error because name is missing
  }
};

In addition, the query methods could be updated to accept a generic parameter which is resolved as it's primary data attribute type, e.g.:

type query<T> = () => Promise<{
  attributes: T;
}>;

This allows the developer to do query<MyAttributes>() and get an object which attributes match keys of MyAttributes.

Here we miss relationships though. I haven't used orbit enough to know if developers would also want better types for relationships. From a quick introduction i found missing attribute types to be annoying.