Typescript decorator to handle Unsupported operations
Simplify your code
Context
Typescript: 4.3.5
In my Angular project, I have a generic class for CRUD services. This class defines all available api calls.
_type IdentifierType_ = _string_ | _number_;
@Injectable()
_export abstract class_ CrudService {
_protected_ _url!: _string_;
_protected constructor_(_protected readonly_ _httpClient: HttpClient) {}
_public_ get<ReturnType>(
identifier: _IdentifierType_
): Observable<_Nullable_<ReturnType>> {
...
}
_public delete_(identifier: _IdentifierType_): Observable<_boolean_> {
_..._
}
_public_ post<ReturnType, BodyType = _Partial_<ReturnType>>(
body: BodyType
): Observable<_Nullable_<ReturnType>> {
_..._
}
_public_ getAll<ReturnType>(
filters?: _Record_<_string_, _string_ | _number_>
): Observable<_Nullable_<ReturnType[]>> {
...
}
_public_ put<BodyType>(body: BodyType): Observable<_boolean_> {
...
}
}
This is usefull when you have the chance to work with a backend api which has the same behavior for all entities. But it’s not always the case…
Problems
Here’s some cases which corresponds to my project reality, probably you will find here some similarities with your own projects:
- An entity cannot be created / deleted, data comes from a reference base or from a configuration base (list of localities, list of products which are not handle by your application…).
- An entity cannot be updated / partially updated, you work in a system which needs to historize all
- Or simplier, just because your backend api does not offer this possibilities yet. You work on a project from scratch and with iterations.
So how to avoid wasting time to debug or to understand in which case you are when something wrong happened ?
Solution
The first approach will probably be to throw an error in the sub classes’ methods like this:
@Injectable()
_export class Person_CrudService extends CrudService {
_protected_ _url = '/persons';
_public override delete_(identifier: _number_): Observable<_boolean_> {
_throw new Error('Unsupported Operation');_
}
_public override_ post<Person>(body: Person): Observable<_Nullable_<Person>> {
_throw new Error('Unsupported Operation');_
}
}
Here we forbidden the access to delete and post operations. We need to rewrite the method’s definitions and to throw an error inside both. Now let’s imagine to do that for each entity… As I’m a bit Lazy and I don’t find it elegant, I chose another way: A typescript class decorator.
It simply takes the list of operations which must throw the expected error.
Type corresponds to the class and allows to get autocompletion when you type the operation name(s).
Here’s the corresponding example:
@Injectable()
_@UnsupportedOperations<Person_CrudService>('delete', 'post')
_export class Person_ CrudService extends CrudService {
_protected_ _url = '/persons';
}
As you can see, when you write this, you save time and some lines of code.
Conclusion
When something to do is boring, not elegant or take to much time then you should probably find a better way to do it. Maybe this solution will not please everyone but I and my team think that’s effective.
Thanks for reading, feel free to comment. See you