-
Notifications
You must be signed in to change notification settings - Fork 17
Description
🚀 Feature Proposal
ErrorFilter
feature is implemented on top of a single not-so-true premise:
"Given a
an instance of type A
, a instanceof A
is guaranteed to be true".
While this is true more often than not, there're some caveats. Phantom dependencies might lead to multiple A
modules, multiple A
constructors, meaning a instanceof A
might be false in some unlucky scenarios.
It looks reasonable to provide an alternative way to deal with this issue.
How do we deal with this problem in vanilla JavaScript?
This problem is not new, and neigther is a good enough solution to it: duck typing. This approach of asuming an object belongs to a given type if it has certain method / properties satisfying a set of conditions is my inspiration to solve the issue:
Proposal: DiscriminatedError
If we can provide a hint to efficiently duck type an Error, we can avoid instanceof
usage while accomplishing a performance friendly validation:
const errorDiscriminatorReflectKey: unique symbol = Symbol.for('@inversifyjs/validation-common/errorDiscriminator');
function Discriminated(value: string | symbol): ClassDecorator {
return (target: Function): void {
// Set property associated to target at errorDiscriminatorReflectKey
}
}
This way, adapters can fetch target error discriminator metadata. If found, adapters can keep a map of discriminators to Errors. When an error is thrown, we can attempt to get the associated error getting it from its discriminator via accessing the error type metadata. In the worst case, we can always fall back to the instanceof
approach if no Error is found this way.
Error hierarchies
Consider the following errors:
@Discriminated('foo')
class FooError extends Error {}
@Discriminated('foo-child')
class FooChildError extends FooError {}
Internally:
FooError
discriminator metadata is an array with one value: ['foo'].FooChildError
discriminator metadata is an array with two values: ['foo-child', 'foo'].- When a
FilterError
catchesFooError
errors, it's associated to the discriminator map to the keyfoo
. - When a
FilterError
catchesFooChildError
errors, it's associated to the discriminator map to the keyfoo-child
.
When a FooChildError
is thrown, the adapter reads it's constructor error discriminator metadata and finds ['foo-child', 'foo'].
- The adapter first attemps to find filters for
foo-child
. If aFilterError
handlesFooChildError
, it will be selected. - If not, the adapter attemps to find filters for
foo
. If aFilterError
handlesFooError
, it will be selected. - If not, the adapter attemps to find Error filters with the current
instanceof
approach.
Motivation
It solves an ErrorFilter related feature as described in the proposal.
Example: Implementing a DiscriminatedError
const myAwesomeDiscriminatedErrorDiscriminator: string =
'my-awesome-unique-discriminator-value';
@Discriminated(myAwesomeDiscriminatedErrorDiscriminator)
class MyAwesomeDiscrimatedError extends Error implements DiscriminatedError { }
Pros and cons
Pros
- It can be implemented in a completelly transparent way. Developers implementing
ErrorFilter
classes forFooError
don't need to care of whether or not the error is discriminated. Developers extending aDiscriminatedError
don't need to be aware of its discriminated nature. Developers implementing aDiscriminatedError
that extends a base class don't need to update the base class to be discriminated. - It's performant friendly: we can identify error filters in a few checks. We don't need to traverse the full type hierarchy.
Cons
ErrorFilter
is now a feature user's need to care about implementation details. We could probably solve this enforcing every cached error must be discriminated, but the tradeof would be negative imo.