feat: mixin system alongside declaration merging#5648
feat: mixin system alongside declaration merging#5648voxpelli wants to merge 4 commits intofastify:mainfrom
Conversation
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.
|
Been looking at how this can interact with the 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 Its as far as I know not possible to:
Actually, even assigning the result from If one were to do: const app = fastify()
app.register(pluginA)
app.register(pluginB)Then one could kind of type them as 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 Maybe a |
|
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 |
|
Related comments for plugin and this approach. |
|
@climba03003 That's more a comment for #5061 I think, as that issue is named 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 |
If the plugin types is not updated accordingly to this change, it will still pollute the types globally. |
This is the approach I took: fastify/fastify-static#469 |
It doesn't conflict my proposed changes to plugin types. That is why I proposed the |
|
@climba03003 Added a rough example on how my approach can provide both a "polluted" global and a type guarded global |
|
If this going to be landed.
For 1. , it is not really my concerns. Both can archive the same goal and utilize your PR here. For 2. , the consumer can choose which plugin is globally available. |
|
@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 I now also added a declare module 'fastify' {
interface FastifyInstance extends AllMixinValues<FastifyInstanceMixins> {}
} |
|
Closing as too old |
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
umzeptionmodule, 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 forhasPlugin()) 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
npm run testandnpm run benchmarkdocumentation is changed or addedNeeds to be doneand the Code of conduct