How can I extend interface with saving its generic types without re-declaring them?
For example I have following code:
interface Controller<
Type extends Record<string, any>,
IdType extends number | string,
UpdateType extends Record<string, any>
> {
get(id: IdType): Type;
remove(id: IdType): Type;
update(id: IdType, data: UpdateType): Type
}
And I need to create other interfaces that inherit the same generic types. The way I am doing it right now is by re-declaring all this types, like this.
interface VotableController<
Type extends Record<string, any>,
IdType extends number | string,
UpdateType extends Record<string, any>
> extends Controller<Type, IdType, UpdateType>{
vote(id: IdType): Type;
unvote(id: IdType): Type;
}
inteface UserEditableController<
Type extends Record<string, any>,
IdType extends number | string,
UpdateType extends Record<string, any>
> extends Omit<Controller<Type, IdType, UpdateType>, "update" | "remove"> {
update(id: IdType, data: UpdateType, userId: number): Type;
remove(id: IdType, userId: number): Type;
}
I want to know is it possible to somehow inherit types Type, IdType and UpdateType, without copy pasting them every time to new interface.
It would be good to do it like this, but it gives an error
interface FooController extends Controller {
foo(): Type
}
// Error: ts(2707) Generic type 'Controller<Type, IdType, UpdateType>' requires between 2 and 3 type arguments
CodePudding user response:
Technically there is a way but it involves some boilerplate and you need to learn a library XD. Needless to say I can't recommend it as it is, unless you really repeat yourself a LOT.
I am working on automating a lot of this with type transformers so conceptually the idea is not bad. Only half-backed.
The idea is to take a side step to a realm which supports threading arguments through, then convert back to normal types.
The first step is to lift Controller into a free type (a type constructor which 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 is done, we can create a free type that inherits form it. This is where the value of all of this lies: we don't repeat ourselves any more:
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;
}
// util to pass arguments through while making constraints bubble up
interface $PassThrough<<$T extends Type> extends Type {
super: apply<$T, this['arguments']> // this['super'] comes from here
constraints: $T['constraints']
}
The last step is to apply it with arguments. This is done with apply
type OK = apply<$VotableController, [{foo: number}, number, {foo: string}]>
apply checks type constraints
// @ts-expect-error: not [Record<string, any>, string | number, Record<string, any>]
type NotOK = apply<$VotableController, [1, 2, 3]>
// ~~~~~~~~~
Example of use 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 }
}
As for UserEditableController, if you want to keep using Omit, you can use Flow to compose free types and cook 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>
}
It can then be used the same way
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;
}
The documentation and a guide can be found on the github repo
