Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FluidObject utility type to Replace IFluidObject #8087

Merged
merged 19 commits into from
Nov 15, 2021

Conversation

anthony-murphy
Copy link
Contributor

@anthony-murphy anthony-murphy commented Nov 2, 2021

This change introduces the FluidObject utility type which will be a replacement for IFluidObject

fixes #8072 fixes #8073

@anthony-murphy anthony-murphy requested a review from a team as a code owner November 2, 2021 00:32
@github-actions github-actions bot added area: definitions public api change Changes to a public API labels Nov 2, 2021
@github-actions github-actions bot requested review from vladsud and curtisman and removed request for a team November 2, 2021 00:33
@vladsud vladsud requested a review from skylerjokiel November 2, 2021 23:32
useProvider(provider.IFluidLoadable);
useLoadable(provider);
useLoadable(provider.IFluidLoadable);
expectError(provider.handle);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don not like this part (.handle & IFluidLoadable loading being on same object).
To me - provider and implementer are two different concepts that should not be mixed (on consuming side) into single object / interface (i.e. IFluidLoadable should not extend IProvideFluidLoadable, and any mechanism we implement should not expose IProvideFluidLoadable & IFluidLoadable as if it were one and the same). Whoever implements IProvideFluidLoadable is free to implement both interfaces, but consumer should not assume that, allowing provider to delegate capabilities of other objects.

So when I see reference to IProvideFoo.IFoo.IFoo.IFoo above, I cringe

Here, I like IUnknonw.QueryInterface and aggregation rules - it is free to return you different objects, but there are some rules RE asking for same interface (you should get same object no matter what object from aggregations was asked for it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree with this entirely, but i don't have a solution, and think it is out of scope for the work i'm doing here. I'm really just trying to decouple the typing rats nest we get into via module augmentation and IFluidObject. In general, i'd also like to reduce our reliance on the pattern, as it should really only be used for feature discover on anonymous types. Ideally, we can get the runtime to a state where this type of pattern is not central, and we can let consumers bring their own pattern here if they need one.

specifically, I'm solving:
#2566
#3264

@github-actions github-actions bot requested a review from vladsud November 3, 2021 18:10
// Warning: (ae-incompatible-release-tags) The symbol "Provider" is marked as @public, but its signature references "ProviderPropertyKeys" which is marked as @internal
//
// @public
export type Provider<T = unknown> = Partial<Pick<T, ProviderPropertyKeys<T>>>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering renaming this to FluidObject, rather than Provider as i think it may make the transition easier for people.
Open to opinions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vladsud and @markfields. what do you think here. i'm actually leaning towards the rename to IFluidObject, as i think it will be a less disruptive change for people.

Copy link
Member

@markfields markfields left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hoping I'm wrong, but when I try this on TS Playground, it only seems to work if IFoo extends IProvideFoo. Can you try adding tests for IFluidRunnable? That one seems to be an example where we're actually using the indirection capability of this paradigm (it doesn't extend IProvideFluidRunnable itself)

...if IFluidRunnable is even used, I didn't see any uses in our repo. Nonetheless, my understanding was that the indirection is an important feature, so would be good to account for it. Personally I haven't been able to come up with anything besides the obvious Partial<IProvideFoo> that works across the board.

@markfields
Copy link
Member

Did some brainstorming about supporting IFoo that doesn't extend IProvideFoo. Not a polished solution, but wanted to share something that worked (TS playground):

type ProvidedBy<T> = { __providerType?: T };

type Provider<T> = 
    T extends ProvidedBy<infer P>
        ? Partial<P>
        : never;

interface IProvideFoo {
    IFoo: IFoo;
}

interface IFoo extends ProvidedBy<IProvideFoo> {
    x: number;
}

function tryit(q: unknown) {
    const maybeFoo = q as Provider<IFoo>;
    console.log(maybeFoo.IFoo?.x);
}

In cases where IFoo extends IProvideFoo, it would be nice to not need to also extend ProvidedBy and use your approach, if they can be combined in an uber Provider type somehow.

@anthony-murphy
Copy link
Contributor Author

I'm hoping I'm wrong, but when I try this on TS Playground, it only seems to work if IFoo extends IProvideFoo. Can you try adding tests for IFluidRunnable? That one seems to be an example where we're actually using the indirection capability of this paradigm (it doesn't extend IProvideFluidRunnable itself)

...if IFluidRunnable is even used, I didn't see any uses in our repo. Nonetheless, my understanding was that the indirection is an important feature, so would be good to account for it. Personally I haven't been able to come up with anything besides the obvious Partial<IProvideFoo> that works across the board.

per the current pattern IFoo should always extend IProvideFoo. This isn't a change.

@markfields markfields dismissed their stale review November 9, 2021 17:54

per the current pattern IFoo should always extend IProvideFoo. This isn't a change.

@markfields
Copy link
Member

per the current pattern IFoo should always extend IProvideFoo. This isn't a change.

Really? When did this change? When I was learning about IFluidObject last summer I thought that was an important design point.

@vladsud
Copy link
Contributor

vladsud commented Nov 9, 2021

per the current pattern IFoo should always extend IProvideFoo. This isn't a change.

I'm with Mark here, I think this should not be assumed or required. Provide patterns should allow aggregation, similar to COM Aggregation.

@anthony-murphy
Copy link
Contributor Author

anthony-murphy commented Nov 9, 2021

per the current pattern IFoo should always extend IProvideFoo. This isn't a change.

I'm with Mark here, I think this should not be assumed or required. Provide patterns should allow aggregation, similar to COM Aggregation.

It has been required due to our request pattern primarily, and is how all our interfaces have always worked, not that some aren't poorly formed, but those are not in use.

The reasons it's needed is we always supported returning an IFoo or an IProvideFoo, but since the requestor doesn't know which as the consumer, they both need the property IFoo which returns the real object. As the IFoo property is what we inspect to get at the real object. In general, we overuse the pattern in our code base where it's not necessary.

It has also always been possible to use 3 interfaces, if the consumer wishes to have an implementation only that doesn't provide itself.

Lastly, i agree with not liking the pattern. This is an incremental change to get out of the typing hell created with module augmentation, and is generally compatible with existing usage. Specifically, it is a typing only change the underlying javascript doesn't change, as the types do not exist there. Not breaking runtime javascript is an explicit goal here

Also, since these are just types, they really only exist for convivence. so users can always cast their way out of it just like they can now.

@anthony-murphy
Copy link
Contributor Author

Did some brainstorming about supporting IFoo that doesn't extend IProvideFoo. Not a polished solution, but wanted to share something that worked (TS playground):

type ProvidedBy<T> = { __providerType?: T };

type Provider<T> = 
    T extends ProvidedBy<infer P>
        ? Partial<P>
        : never;

interface IProvideFoo {
    IFoo: IFoo;
}

interface IFoo extends ProvidedBy<IProvideFoo> {
    x: number;
}

function tryit(q: unknown) {
    const maybeFoo = q as Provider<IFoo>;
    console.log(maybeFoo.IFoo?.x);
}

In cases where IFoo extends IProvideFoo, it would be nice to not need to also extend ProvidedBy and use your approach, if they can be combined in an uber Provider type somehow.

this is a javascript breaking change

@markfields
Copy link
Member

this is a javascript breaking change

I don't think so... it's all type and interface. The top one isn't adding an actual property to any real object. I just needed to use the generic param T somehow. It's optional so implementers can and should ignore it.

@anthony-murphy
Copy link
Contributor Author

anthony-murphy commented Nov 9, 2021

this is a javascript breaking change

I don't think so... it's all type and interface. The top one isn't adding an actual property to any real object. I just needed to use the generic param T somehow. It's optional so implementers can and should ignore it.

__providerType is a real property on the type, even if it is optional. This also requires all the existing interfaces to be rebuilt, which is a huge breaking change. the current change here works with existing interfaces

@anthony-murphy
Copy link
Contributor Author

@vladsud and @markfields what do we need to close here? I'm really trying to keep this change scoped to the dependency issues created via module augmentation. The current IFluidObject is unsustainable due to this. I'm explicitly not trying to build a new discovery pattern from the one we have today.

For the discovery pattern, i'd like for that to just not be a thing that we have to own, and instead an application pattern. I think things like #8169 could help get use there

Copy link
Member

@markfields markfields left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is okay. I also wonder about telling people simply to cast to Partial<IProvideFoo>, and avoiding the complexity and limitations here.

If you do go ahead with this more complex Provider type, then I do think you should rename it to FluidObject. Biggest reason is that delegation using a separate implementation of IProvideFoo from IFoo is not supported, so it's not as flexible as "Provider" would lead me to expect. i.e. Since you need to do such a special dance to participate in this pattern, a more specific name is better.

Other feedback - I don't know that supporting FluidObject<IProvideFoo> is that valuable, do you have a case in mind from talking to Office Fluid folks? FluidObject<IFoo> reads much better (and see above about not supporting all IProvideFoo). I don't think you need to change anything in the code, just more about in terms of how it's documented/explained in comments etc.

Taking a step further... In fact, you don't need to write an IProvideFoo at all to participate here. IFoo just needs a member that returns an IFoo (and the convention to name it IFoo is nice). So if we're ditching delegation as you say, then it would be cool if we could stop publicizing anything about IProvide interfaces too, since that leads my mind towards a general-purpose delegation pattern.

Again... unless we let go of all this scaffolding and just recommend Partial<IProvideFoo> which is all good.

useLoadable(foo.IFluidLoadable);
expectError(foo.handle);
foo.IFluidLoadable?.handle;
const unknown: Provider | undefined = foo.IFluidLoadable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this test scenario. Why are you giving foo.IFluidLoadable the type Provider | undefined? Shouldn't it be IFluidLoadable | undefined? And why call it unknown, instead of loadable or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is about testing implicit conversion between an unspecified, or unknown Provider and a concrete or known Provider<IFoo>.

Think of the example of scope that the loader takes. It will take scope: Provider and i don't want to require a cast for a consumer to set scope to Provider<IFoo>. Similarly, within the runtime, when a consumer accesses scope i don't want to require a cast either, so want to support const maybeFoo: Provider<IFoo> = contenxt.scope

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you are just hung up on the naming here. nothing here has to do with the typescript type unknown as it is not useful in this context

common/lib/core-interfaces/test-d/index.test-d.ts Outdated Show resolved Hide resolved
interface IProvideFoo{
IFoo: IFoo;
}
interface IFoo extends Partial<IProvideFoo>{
Copy link
Contributor Author

@anthony-murphy anthony-murphy Nov 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vladsud and @markfields i've added a new test which verifies that the new utility type, which i've renamed from Provider to FluidObject also works if the provider is partial/optional on the implementation. This means we only need the type to specify it, and we do not need the implementation to actually have it.

While not perfect, i do think this is closer to the pattern you also prefer where provider and implementor are decoupled.

also, @barnawhi. the partial thing may make functional objects work a bit better

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when I try this in TS Playground, it doesn't work. And that makes sense to me - your Keys type will be doing keyof (IFoo | undefined) (line 27) which is never. Not sure why your tests pass -_-

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird. not sure why there is an error there, but not the test, but regardless nice find. fix it with an exclude

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works too:

TProp extends keyof Required<T>[TProp]

Maybe you don't have strict null checks enabled something weird like that?

@anthony-murphy anthony-murphy changed the title Add Provider Interface to Replace IFluidObject Add FluidObject utility type to Replace IFluidObject Nov 10, 2021
Copy link
Member

@markfields markfields left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎂 Really cool stuff, way to go! And thanks for taking our feedback and figuring out how to support delegation too.

common/lib/core-interfaces/src/provider.ts Show resolved Hide resolved
common/lib/core-interfaces/src/provider.ts Outdated Show resolved Hide resolved
common/lib/core-interfaces/src/provider.ts Outdated Show resolved Hide resolved
* foobar();
* }
* ```
* This pattern enables discovery, and delegation in a standard way which is central
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really glad you found a good a way to accommodate proper delegation, yay! Can you update this doc comment to cover at least briefly about using Partial<IProvideFoo> if you want to do proper delegation? And I'm with Vlad - mentioning IProvideFoo.IFoo.IFoo doesn't necessarily bring clarity. That is indeed how the type magic works (it finds any prop that also exists on the prop's type), but not how this stuff is ever used.

anthony-murphy and others added 6 commits November 15, 2021 09:29
Co-authored-by: Mark Fields <markfields@users.noreply.github.com>
Co-authored-by: Mark Fields <markfields@users.noreply.github.com>
Co-authored-by: Mark Fields <markfields@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mark IFluidObject as deprecated Add provider pattern types to core interfaces
3 participants