-
Notifications
You must be signed in to change notification settings - Fork 15
Terse API for Mixin #22
Comments
I'm wondering about the implications of the API example you provided. It looks like the main The second thought I had is in relation to providing an array of mixins, and my thinking is definitely being influenced by dgrid here so I may well be overlooking other use cases. It seems more likely that somebody would want to pass a single mixin, aspect, and init function (or some combination thereof), rather than passing several mixins with one aspect object and one init function. I can see the usefulness of providing the overloads for passing multiple mixins though. On the tersemixin branch, currently the types are as follows: /* Mixin API */
export interface ComposeClassMixin<O, P> {
base?: GenericClass<P>;
initializer?: ComposeInitializationFunction<O, P>;
aspectAdvice?: AspectAdvice;
}
export interface ComposeFactoryMixin<O, P, T> {
base?: ComposeFactory<T, P>;
initializer?: ComposeInitializationFunction<O, P>;
aspectAdvice?: AspectAdvice;
}
export interface ComposeFactory<O, T> {
mixin<U, V>(mixin: ComposeClassMixin<V, U>): ComposeFactory<O, T & U>;
mixin<P, U, V>(mixin: ComposeFactoryMixin<V, U, P>): ComposeFactory<O & P, T & U>;
}
export interface Compose {
mixin<O, A, P, B>(base: ComposeFactory<O, A>, mixin: ComposeClassMixin<P, B>): ComposeFactory<O, A & B>;
mixin<O, P, A, B, C>(base: ComposeFactory<O, A>, mixin: ComposeFactoryMixin<C, B, P>): ComposeFactory<O & P, A & B>;
}
function mixin<O, A, P, B>(base: ComposeFactory<O, A>, mixin: ComposeClassMixin<P, B>): ComposeFactory<O, A & B>;
function mixin<A, B, O, P, T>(base: ComposeFactory<A, O>, mixin: ComposeFactoryMixin<T, P, B>): ComposeFactory<A & B, O & P>;
} In order to handle the equivalent to the export interface Compose {
// The existing signatures
mixin<O, A, P, B>(base: ComposeFactory<O, A>, mixin: ComposeClassMixin<P, B>): ComposeFactory<O, A & B>;
mixin<O, P, A, B, T>(base: ComposeFactory<O, A>, mixin: ComposeFactoryMixin<T, B, P>): ComposeFactory<O & P, A & B>;
// Overloads for multiple mixins
mixin<O, A, P, B, Q, C>(base: ComposeFactory<O, A>, mixin: ComposeClassMixin<P, B>, secondMixin: ComposeClassMixin<Q, C>): ComposeFactory<O, A & B & C>;
mixin<O, P, A, B, Q, C, T, U>(base: ComposeFactory<O, A>, mixin: ComposeFactoryMixin<T, B, P>, secondMixin: ComposeFactoryMixin<U, C, Q>): ComposeFactory<O & P & Q, A & B & C>;
} |
Sorry, back into things again. Your points are valid...
I was thinking outloud versus being pendantic... I would say though let's not be afraid to change the main API if it deals with the main use case of the library. The feedback as I understood was that to we were finding the get X done, it was too verbose and actually we would like to throw something specific at it which just sort of "works" that was as "approachable" as Dojo1's declare. I would suggesting thinking of it though from how you would like to write the code and then we can try to figure out how to make it work with type inference. What I think you are saying is you feel it works better like this: const fooFactory = compose({
foo: 'bar'
}, fooInit);
const fooBarFactory = mixin(fooFactory, {
mixins: [ Bar, bazFactory, { qat: 2 } ],
aspect: { /* ... */ },
init: initFunction
});
/* or */
const fooBarFactory = compose({
foo: 'bar'
})
.mixin({
mixins: [ Bar, bazFactory, { qat: 2 } ],
aspect: { /* ... */ },
init: initFunction
}); If we think that the majority of the operations are going to be just taking an object literal with an init function and creating a factory, then I would say, let's not touch the |
I do think we will still want to be able to support the base case of an object and an optional init function with the The only part where there's still a difference between the current implementation and your latest code example is where multiple mixins are passed. const fooBarFactory = compose({
foo: 'bar'
})
.mixin({
mixins: [ Bar, bazFactory, { qat: 2 } ],
aspect: { /* ... */ },
init: initFunction
}); Would currently be const fooBarFactory = compose({
foo: 'bar'
})
.mixin(
{
base: Bar,
aspect: { /* ... */ },
init: initFunction
},
{
base: bazFactory
},
{
base: { qat: 2 }
}
); This is obviously a little more verbose for this case, but if all you want to do is add multiple objects to modify the type, Edit: Fixed a typo in the example code |
@maier49 and I discussed this verbally, summary of the conversation was:
|
TL;DR your suggestion around mixin works! @maier49 in using compose, I am finding often when I have a mixin, I want/need to provide an init function, because there is a feature in that mixin which I wan't to use. I don't know why before this we hadn't noticed it, but it is really awkward. For example, if I have something like this: import compose from 'compose';
const createDestroyable = compose({
own(handle: Handle): void {
/* own a handle */
},
destroy(): void {
/* destroy handles */
}
}, () => { /* init handle array */ });
const createSomething = compose({
foo: null
})
.mixin(createDestroyable); I want to be able to create something with a handle and have it owned during the construction of |
@maier49 first, really good work! Some feedback on the For the initializer function in a mixin, you don't get the right type inferred for options, but I would expect to be able to. For example: interface AOptions {
foo?: string;
}
const createB = compose({ bar: 1 });
const createA = compose<AOptions, { foo: string; }>({ foo: 'bar' },
(instance, options) => { /* instance and options correct */ })
.mixin({
mixin: createB,
initializer(instance, options) {
/* options will be of type {} */
/* instance will be of type { foo: string; } & { bar: number; } */
/* options should be of type AOptions & {} */
}
}); Also, I know we talked offline about the ordering of the init functions and the only one we had left up in the are is the base initializer. Taking a look at using this, I realised that while mixins should generally be isolated from each other, I found a use case where my methods in my base depended on features from mixins and in my init function, I called some of those methods. The expectation would then be that those methods it would call would not fail, therefore it would have expected that base initializer would have run last. It would be "nice" to have const createWidget: WidgetFactory = compose(createEvented)
.mixin({
mixin: createDestroyable
})
.mixin({
mixin: createRenderable
})
.mixin({
mixin: createStateful
}); Or the following: const createWidget: WidgetFactory = compose(createEvented)
.mixin({
mixins: [ createDestroyable, createRenderable, createStateful ]
}); I am not suggesting getting rid of the interface ComposeFactory<O, T> {
(options?: O): T;
}
interface GenericClass<T> {
new (...args: any[]): T;
prototype: T;
}
interface ComposeInitializationFunction<O, T> {
(instance: T, options?: O): void;
}
interface AspectAdvice { }
type ComposeMixinItem<O, T> = GenericClass<T> | T | ComposeFactory<O, T>;
interface ComposeMixin<O, P, Q, R, S, T, U, V, W, X> {
mixins?: [ ComposeMixinItem<P, U>, ComposeMixinItem<Q, V> ]
| [ ComposeMixinItem<P, U>, ComposeMixinItem<Q, V>, ComposeMixinItem<R, W> ]
| [ ComposeMixinItem<P, U>, ComposeMixinItem<Q, V>, ComposeMixinItem<R, W>, ComposeMixinItem<S, X> ]
mixin?: ComposeMixinItem<P, U>;
initializer?: ComposeInitializationFunction<O & P & Q & R & S, T & U & V & W & X>;
aspectAdvice?: AspectAdvice;
}
function mixin<O, P, Q, R, S, T, U, V, W, X>(mixin: ComposeMixin<O, P, Q, R, S, T, U, V, W, X>): ComposeFactory<O & P & Q & R & S, T & U & V & W & X> {
return; /* who cares about details*/
}
const createSomething = mixin({
mixins: [ { foo: 'bar' }, { bar: 12 }, { baz: false }, { qat: / / } ],
initializer(instance, options) {
instance.foo;
instance.bar;
instance.baz;
instance.qat;
}
});
const something = createSomething();
something.foo;
something.bar;
something.baz;
something.qat; In using the tuples, the type inference appears to collapse and the resulting type is inferred correctly. One final though... I am realising that the generic ordering we have in compose is a bit silly... Originally I had that |
These issues should be addressed now. The argument to The type of the options argument is now being properly inferred from the base and mixin. One thing that I noticed though, is that the unspecified generics are taking the empty object type, so for example with something like: compose({
foo: ''
}, function (instance: { foo: string }, options: { foo?: string } ) {
if (options.foo) {
instance.foo = options.foo;
}
}).mixin({
mixin: { bar: number },
initializer: function (instance, options) {
}
}); The inferred type of the instance and options arguments of initializer would be, respectively: Allowing only one mixin to be passed at a time helps, but doesn't completely resolve this problem. It helps because as long as you pass a compose factory then the instance and options type can be inferred from that and there are no extra generic types to collapse to {}. Where it doesn't help is when a generic class or a plain object is used as the mixin instead of a ComposeFactory. In that case the options type will still end up being inferred to Going back to overloads would eliminate this problem, but then we are back at the problem of that pushing complexity onto users of the library. |
@maier49 first, Happy Birthday! 🎂 🍰 Second, I took a look and agree that type inference on options is always collapsing to I started to add some JSDoc to the I think the type I ran into a problem when passing factories in the |
Based on our discussions on the issue, I removed the Also based on our discussion, I overloaded the mixin signature to allow for anything with a The
|
Closed via 77042c0 |
The review of dojo/compose by the SitePen/dgrid (@maier49, @kfranqueiro, and @edhager) as highlighted that the mixin API should/could be significantly more terse, especially when dealing with complex type compositing. Ideally the API would work more like this:
I expect the interface would need to be something like this:
The text was updated successfully, but these errors were encountered: