canjs / can-observe

Observable objects
https://canjs.com/doc/can-observe.html
MIT License
20 stars 2 forks source link

RFP: Type System #62

Closed christopherjbaker closed 5 years ago

christopherjbaker commented 6 years ago

can-observe needs a type system. At the very least, it should support as much as can-define (string, date, number, boolean, Type).

What needs to be decided:

Examples:

@checkTypes({ // validation
  name: 'string', // string specifier
  when: Date, // constructor specifier
}) // many types
class Type extends ObserveObject {
  @coerceType('string') // coercion with string specifier
  title = null // inline decorators will require setting default values

  @coerceType({ // coercion with expanded object specifier
    type: 'number', // with string specifier
    optional: false, // disallow `null`
    maximum: 100, // extra configs for `number` type
  })
  count = 0
}
christopherjbaker commented 6 years ago

@BigAB @mickmcgrath13 @DesignByOnyx @justinbmeyer You've all expressed an interest in a type system for can-observe. Any thoughts?

matthewp commented 6 years ago

Can we put that in a separate library? imo it would be nice of there were proxy based observables that didn't have rigid type definitions and then types could be added on top, probably via decorators like in your example. I would think these decorators would be agnostic of the constructor/class they are extending, so they wouldn't need to be aware of can-observe.

christopherjbaker commented 6 years ago

@matthewp I'm not opposed to putting them in their own repo as long as they are usable on their own. Do you mean essentially that the above example doesn't extend ObserveObject but that the type system still workes with the naked class?

BigAB commented 6 years ago

I'm just wondering, do we need a runtime type system?

Would a static type checker (like flow or typescript) give us much of what we want, without re-inventing the wheel?

Of course static type checking wouldn't help with coercions, but maybe that could be it's own thing?

justinbmeyer commented 6 years ago

@BigAB a runtime type system gives type checking to people who aren't using flow / typescript (likely the vast majority of JS devs).

Also, I see this as similar to enforcement of "required" properties which is pretty common across frameworks even though "required" properties are solved by static type checking languages.

justinbmeyer commented 6 years ago

@matthewp I think it would be GREAT if this could be a standalone util. But I think there might be a few difficulties making something work for can-define, can-observe, and everything else ...

The big question w/ getters and setters is "What are you doing with the value"? What I mean by that is if you do a basic getter / setter, you need to store the value somewhere else. The following more or less what can-define does (if "foo" must be a string):

class Type(){
  constructor(){
    this._data = {}
  }
  get foo(){ return this._data.foo }
  set foo(newVal) { 
    this._data.foo = canReflect.isMember( newVal, StringType) ?
     newVal : canReflect.new(StringType, [newVal]); 
  }
}

These getter/setters will need to know where to store the value. It might be possible to use weakmaps (with some performance cost). But can-observe's Proxies have the underlying object to save values on.

Another problem will be keeping these things observable. If we added a getter / setter on the prototype, we need getting to call ObservationRecorder.add() and setting to fire events. I'm not sure how this would work with can-observe. With can-define, we basically wire up a single getter and setter. The type conversion:

return canReflect.isMember( newVal, StringType) ? newVal : canReflect.new(StringType, [newVal])`) 

Happens before the storing:

this._data.foo = RESULT;

Due to this complexity, I would build these decorators to work with can-observe first. Once it's working, lets see if we can make something more generally useful.

justinbmeyer commented 6 years ago

Here's my opinion on those questions:

christopherjbaker commented 5 years ago

If we are going to offer both validation and coercion, I think we should make it extremely clear which is chosen by making it two different functions: validateType and coerceType. Otherwise I think we should stick to validation.

I don't really have an opinion on the availability of string-based type specifiers, but I do think we should make it exceptionally easy to validate/coerce more complicated things like specific string patterns (such as phone numbers). I think a function is easy enough here, which can throw an error for validation fails and return a new value for coercion, or both for values that can't be coerced.

I think it being decorators primarily is fine, though I've found that they have become less popular in the JS ecosystem (I think primarily due to the instability of the proposals). I think the structure specified in this pr is a good one that is very simple to implement and use.

justinbmeyer commented 5 years ago

can-type is going to take over this.