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