Skip to content

feat: mixin system alongside declaration merging#5648

Closed
voxpelli wants to merge 4 commits intofastify:mainfrom
voxpelli:prototype-improved-register
Closed

feat: mixin system alongside declaration merging#5648
voxpelli wants to merge 4 commits intofastify:mainfrom
voxpelli:prototype-improved-register

Conversation

@voxpelli
Copy link
Copy Markdown
Contributor

See #5061 for issue talking about moving away from declaration merging.

This solution is losely based on and an evolution of the system arrived at for my umzeption module, which in turn I extracted into my personal type helpers module and now revamped into a mixin system.

It originally revolved around enabling third-party extendible discriminated unions – similar to basic declaration merging, but utilizing the power of discriminated unions to get around the downsides of declaration merging.

This mixin solution relies a bit less on the discriminated unions aspect and instead adds a new hasMixin() helper (type only for now, will essentially be a type alias for hasPlugin()) that type guards the properties so that they only appear when the module is explicitly available.

This should be possible to extend with some implicit checks – like if plugin B follows the registration of plugin A, then an implied hasMixin('A') should be made.

It should also be possible to add an assertMixin('A') to avoid if-statements and simply throw on missing mixins – TypeScript support both types of type narrowing, but for some reason it didn't immediately accept mine now. I personally rarely use the assert variant – I much rather check and throw an error of my own instead, or handles it more gracefully.

There are some tricks in the mixin helpers that I can expand upon, but its getting late here and I want to wrap this up before the end of the day 🙏

Oh – and this approach has 100% backwards compatibility. All existing declaration merging continues to work. It simply adds an improved way of adding types.

And it works 100% with JSDoc, as that's the kind of types I personally do.

Checklist

See fastify#5061 for issue talking about moving away from declaration merging.

This solution is losely based on and an evolution of the system arrived at for my [`umzeption`](https://github.com/voxpelli/umzeption) module, which in turn I extracted into my [personal type helpers module](https://github.com/voxpelli/type-helpers/blob/main/lib/declaration-types.d.ts) and now revamped into a mixin system.

It originally revolved around enabling third-party extendible discriminated unions – similar to basic declaration merging, but utilizing the power of discriminated unions to get around the downsides of declaration merging.

This mixin solution relies a bit less on the discriminated unions aspect and instead adds a new `hasMixin()` helper (type only for now, will essentially be a type alias for `hasPlugin()`) that type guards the properties so that they only appear when the module is explicitly available.

This should be possible to extend with some implicit checks – like if plugin B follows the registration of plugin A, then an implied `hasMixin('A')` should be made.

It should also be possible to add an `assertMixin('A')` to avoid if-statements and simply throw on missing mixins – TypeScript support both types of type narrowing, but for some reason it didn't immediately accept mine now. I personally rarely use the assert variant – I much rather check and throw an error of my own instead, or handles it more gracefully.

There are some tricks in the mixin helpers that I can expand upon, but its getting late here and I want to wrap this up before the end of the day 🙏

Oh – and this approach has 100% backwards compatibility. All existing declaration merging continues to work. It simply adds an improved way of adding types.

And it works 100% with JSDoc, as that's the kind of types I personally do.
@github-actions github-actions bot added the typescript TypeScript related label Aug 27, 2024
@voxpelli voxpelli changed the title Add a mixin system alongside declaration merging feat: mixin system alongside declaration merging Aug 27, 2024
@voxpelli
Copy link
Copy Markdown
Contributor Author

voxpelli commented Aug 31, 2024

Been looking at how this can interact with the avvio core as well, automatically adding the methods on register and eg. after – a tricky part is that the .use() (.register() in fastify) both modifies and returns the current instant.

In TS one can assert that the instance is of a specific type, see playground and here:

class MagicAssertion {
  assertMagic(): asserts this is { magic: true } {}
}

// Explicit type apparently needed for assertion to work
const magicInstance: MagicAssertion = new MagicAssertion();

// Error
magicInstance.magic;

magicInstance.assertMagic();

// Not an error
magicInstance.magic;

// Its still a MagicAssertion instance as well
magicInstance.assertMagic();

The assertMagic(): asserts this is { magic: true } means that assertMagic() will either throw or return void.

Its as far as I know not possible to:

Actually, even assigning the result from magicInstance.assertMagic() seems to break the assertion.

If one were to do:

const app = fastify()
app.register(pluginA)
app.register(pluginB)

Then one could kind of type them as asserts this is MixinsPluginA to ensure app gets the mixins.

But since registration can happen async, the assertion would need to be able to work async, or else it would tell the wrong story.

And since registrations can currently be chained, the assertions would need to happen as a side effect – returning the instance.

The alternative is to return a new type:

const app = fastify()
const appWithA = app.register(pluginA)
const appWithAB = appWithA.register(pluginB)

This works with chaining:

const appWithAB = fastify().register(pluginA).register(pluginB)

But appWithA and app will both retain their old type, even though in practice, when appWithAB has A and B, so has app and appWithA and at every moment a javascript check appWithAB === app will be truthy, while the type system will believe it to not be truthy.


Maybe a hasMixin() like added in this PR is a good first step. Maybe either TS or Fastify should be changed to enable the rest? Or it should be accepted that types and reality will not be a 1:1?

@voxpelli
Copy link
Copy Markdown
Contributor Author

For reference, this is the docs on type narrowing that goes through type predicates, discriminated unions and links out to the blog post announcing assertion functions (added in TS 3.7, but yet to get a description in the documentation itself): https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

@climba03003
Copy link
Copy Markdown
Member

Related comments for plugin and this approach.
fastify/fastify-static#427 (review)

@voxpelli
Copy link
Copy Markdown
Contributor Author

voxpelli commented Sep 2, 2024

@climba03003 That's more a comment for #5061 I think, as that issue is named Removal of declaration merging

Also: The approach in this PR is different to the one in fastify/fastify-static#427 (review) I believe

The approach in this PR is backwards compatible for publishers and for consumers it could be made so without the need for extra files, eg by having a FastifyInstanceWithMixins type that expose all mixins, or exposing all mixins on FastifyInstance and have a new FastifyInstanceWithoutMixins that works like in this PR

@climba03003
Copy link
Copy Markdown
Member

The approach in this PR is backwards compatible for publishers and for consumers it could be made so without the need for extra files

If the plugin types is not updated accordingly to this change, it will still pollute the types globally.
Plugins should be updated as proposed in @fastify/static.

voxpelli added a commit to voxpelli/fastify-static that referenced this pull request Sep 2, 2024
@voxpelli
Copy link
Copy Markdown
Contributor Author

voxpelli commented Sep 2, 2024

The approach in this PR is backwards compatible for publishers and for consumers it could be made so without the need for extra files

If the plugin types is not updated accordingly to this change, it will still pollute the types globally. Plugins should be updated as proposed in @fastify/static.

This is the approach I took: fastify/fastify-static#469

@climba03003
Copy link
Copy Markdown
Member

climba03003 commented Sep 2, 2024

This is the approach I took: fastify/fastify-static#469

It doesn't conflict my proposed changes to plugin types.
You should allows a way to pollute the global.

That is why I proposed the fastify-global.d.ts and consumer can apply if they need.

@voxpelli
Copy link
Copy Markdown
Contributor Author

voxpelli commented Sep 2, 2024

@climba03003 Added a rough example on how my approach can provide both a "polluted" global and a type guarded global

@climba03003
Copy link
Copy Markdown
Member

climba03003 commented Sep 2, 2024

If this going to be landed.
The plugins should

  1. Provides either mixin types (types: replace declaration merging with mixin types fastify-static#427) or declaration merging the mixin by default (Companion change to fastify PR fastify-static#469)
  2. Provides global types that allows simple global pollution.

For 1. , it is not really my concerns. Both can archive the same goal and utilize your PR here.
Provides mixin types is less automate, but it can be less pollute on the mixin utility types.
Pollute the mixin utility types by default provides better DX, because less works for the consumer.

For 2. , the consumer can choose which plugin is globally available.

@voxpelli
Copy link
Copy Markdown
Contributor Author

voxpelli commented Sep 2, 2024

@climba03003 This PR was meant as a suggested solution to #5061 – if declaration merging should still be possible as the default then that's a discussion that belongs there.

But anyway: I added FastifyInstance['withMixins'] as an example of solving 2 through the solution of 1.

I now also added a AllMixinValues<>, using that one can add all mixins to the FastifyInstance by simply doing:

declare module 'fastify' {
  interface FastifyInstance extends AllMixinValues<FastifyInstanceMixins> {}
}

@voxpelli voxpelli marked this pull request as draft September 6, 2024 11:26
@Eomm
Copy link
Copy Markdown
Member

Eomm commented Jan 17, 2026

Closing as too old

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

typescript TypeScript related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants