There is a way to accomplish this, but it requires some boilerplate and learning a new library. I wouldn't recommend it unless you find yourself doing the same thing over and over again.
I am currently developing a method to automate much of this process using type transformers. The idea has potential, but it's not fully fleshed out yet.
The concept involves moving into a realm that supports passing arguments through before reverting back to normal types.
The initial step is to elevate Controller
into a free type (a type constructor that can be passed around without parameters):
import { Type, Checked, A, B, C, apply } from 'free-types';
interface $Controller extends Type {
type: Controller<Checked<A, this>, B<this>, Checked<C, this>>
constraints: [
A: Record<string, any>,
B: number | string,
C: Record<string, any>
]
}
Once this foundation is laid, we can create a free type that inherits from it. This eliminates the need for repetitive code:
interface $VotableController extends $PassThrough<<$Controller> {
type: this['super'] & Voting<this[A], this[B]>
}
type Voting<Type, IdType> = {
vote(id: IdType): Type;
unvote(id: IdType): Type;
}
// utility function that passes arguments through while allowing constraints to trickle up
interface $PassThrough<<$T extends Type> extends Type {
super: apply<$T, this['arguments']> // this['super'] originates here
constraints: $T['constraints']
}
The final step involves applying it with arguments using the apply
function:
type OK = apply<$VotableController, [{foo: number}, number, {foo: string}]>
apply
enforces type requirements
// @ts-expect-error: not [Record<string, any>, string | number, Record<string, any>]
type NotOK = apply<$VotableController, [1, 2, 3]>
// ~~~~~~~~~
Here's an example of how this can be used with a class:
class Foo<
Type extends Record<string, any>,
IdType extends number | string,
UpdateType extends Record<string, any>
> implements apply<$VotableController, [Type, IdType, UpdateType]> {
constructor (private a: Type, private b: IdType, private c: UpdateType) {}
get(id: IdType) { return this.a }
remove(id: IdType) { return this.a }
update(id: IdType, data: UpdateType) { return this.a }
vote(id: IdType) { return this.a }
unvote(id: IdType) { return this.a }
}
For the UserEditableController
, if you wish to continue using Omit
, you can leverage Flow
to compose free types and generate a $ControllerOmitting
free type constructor
import { Flow } from 'free-types';
type $ControllerOmitting<T extends PropertyKey> =
Flow<[$Controller, $Omit<T>]>
interface $Omit<T extends PropertyKey> extends Type<1> {
type: Omit<this[A], T>
}
This can then be implemented in a similar manner
interface $UserEditableController
extends $PassThrough<$ControllerOmitting<'update' | 'remove'>> {
type: this['super'] & Editable<this[A], this[B], this[C]>
}
type Editable<Type, IdType, UpdateType> = {
update(id: IdType, data: UpdateType, userId: number): Type;
remove(id: IdType, userId: number): Type;
}
Further details and a comprehensive guide can be accessed via the GitHub repository linked here