Open mattiamanzati opened 6 years ago
@mattiamanzati interesting ideas! Did you see #774 btw :) it already separates construction snapshot type and normal snapshot type
@mattiamanzati sounds nice. I have two questions:
1) Will those type declarations on model be optional to use?
The thing I'm afraid of is that amount of code to be written by end-users will increase. Moreover describing same thing twice (as type & as implementation) in different places makes changes/refactoring harder (e.g. renaming a computed, or changing signature of an action).
2) Does finalize
mean to substitute actions
/views
/volatile
/extend
?
@k-g-a heres some tips:
Will those type declarations on model be optional to use?
They will be mandatory :)
The thing I'm afraid of is that amount of code to be written by end-users will increase. Moreover describing same thing twice (as type & as implementation) in different places makes changes/refactoring harder (e.g. renaming a computed, or changing signature of an action).
If you think about it, for people using TS this is a win, because you won't need type annotations in the finalizer, as the types will be inferred from the declaration.
Does finalize mean to substitute actions/views/volatile/extend?
actios, views, volatile, etc.. can be aliased to the new syntax and converted back using the new syntax and declaring action as accepting any parameter. I would slowly introduce this change.
...extend?
model.extend will became just an alias for intersection :)
The more I look at it, the better I like it, it solves quite some nasty patterns. Refactoring (not just the MST code base, but all projects that use it in general) will be huge though. Would it it be possible to introduce this as experimental feature, with an api like types.model2(...)
than produces an IType<C, S, T>
so that it would be an alternative syntax instead of a replacement and phase the old one over time if this works better indeed? (For example by using a code mod?)
Ideally the old API can be easily polyfilled to be mapped to the new one; also this should be done in a major release, and as we did from 0.9 to 0.10 I can provide a codemod too :)
We can call the new type "object" or "interface" as it is an interface that contains methods :)
any updates here?
any update?
Preface: This PR contains a lot of breaking things I am looking for feedback, please reply if they make sense or not for your needs. Not sure if they all make sense for a 3.0 release, but I think they would for a next major version like 4.0
Changing from the
IType<S, T>
toIType<A, O = A, I = O, F = A>
Right now the
IType
interface accepts two parameters; the runtime type and the snapshot one.If you think about it, that's not completely true. We need more types to represent an MST type. Here's what they should be:
A
is the runtime type; it should be the ObservableArray, observable object, etc...O
is the output type ofgetSnapshot(instance)
I
is the input type oftype.create(snapshot)
F
is the type used by the finalizer result,type.finalizer((self: A) => F)
, we will talk about it more later.Introducing
action
,computed
andview
typesOne of the recurring isses in MST usage with TS is the typing of "self" when defining actions / views.
At the moment MST requires that they should be declared in a predefined order to make them correctly work. There are also issues when views/action may call each other recursively.
A really nice way to solve this is to include action and view types in props definition, and later require them to have an actual implementation.
As you can see now actions can depend on each other, and be used in code even if not declared yet. But how should I declare the computed/view/action declaration now? That's what finalizer do!
Finalizers are functions that are called upon instance contstruction and may return data to improve/finalized the instance based on how the type is declared. For example, the object type
P
accepts as finalizer aPartial<P>
, and the object type will squash that partial into the object instance once created (exaclty how happens with action and views). In the same way, the computed type (of string) accepts as finalizer value a function returning a value (of string). The action/view/computed has an Input and Output type ofnever
that means that cannot be restored from snapshot, and the object type will automatically remove those properties, as valuewise it is not possible to have a property with "never" as value.Below at the end of the issue there is a working typelevel implementation of the new types.
Once https://github.com/Microsoft/TypeScript/pull/24897#issue-194165511 will be merged in next major, we will be able to convert the type of the argument of the below example from a tuple to a function with that exact tuple as input args.
Lazily wrap actions
Since actions are now indipendent atoms, we can lazily wrap them with MST internals, instead of doing that upon instance creation. This will increase performance as done in #810 / #845 because actions will be wrapped only upon first usage.
Back to structural checking, as TS does
Having atomic action with typed parameters, means we can perform checks also on their result and parameter types. At the moment each types.model call will produce a new "primitive". With the action change, we can introduce structural comparison even on actions, so on models too. This will be more inline with TS, because before TS was ok on assigning different MST types if they have the same signature (bug).
after this change, Point and Coord will be compared structurally, so as long the types are assignable between them, MST (as TS does) will be ok with assigning them.
Faster computeds
As someone pointed out (not really a big problem :P), computed properties are slower to create, with this change the computed implementation is not copied anymore from a getter, but instead already provided as a function. So basically there's 1 computed less the current implementation.
Introduce
intersection
(a.k.a typewide compose)Thanks to these new additions, we could also introduce a type called
intersection
. Intersection is represented at TS level with the&
operator, and basically means that a type will have both the features of two given types. Behind the scenes, it will call create on all types and Object.assign features of the subsequent to the first one. Then all finalizers will be called with "self" being the just constructed object. This will allow also intersection between ES6 maps and objects.For example:
Full working typelevel code