Home > Enterprise >  Is it possible to inherit interface generic types?
Is it possible to inherit interface generic types?

Time:01-14

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;
}

playground

The documentation and a guide can be found on the github repo

  • Related