Can interface generic types be inherited?

Is it possible to extend an interface without re-declaring its generic types?

Consider the 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
}

Creating other interfaces that inherit the same generic types currently involves re-declaring all these types.

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

Is there a way to inherit the types Type, IdType, and UpdateType without repeating them in each new interface?

Attempting to do so results in an error:

interface FooController extends Controller {
   foo(): Type
}
// Error: ts(2707) Generic type 'Controller<Type, IdType, UpdateType>' requires between 2 and 3 type arguments

Answer №1

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I convert a property to an interface in Typescript?

I'm having trouble casting geometryType as I keep getting this error : IShape is a Type not a Namespace when attempting to do the following: interface IShape { readonly geometryType: "RECTANGLE" | "SQUARE" } let geom ...

What could be causing the inability to update a newly logged-in user without refreshing the page?

Hello, I have encountered an issue with my application that involves registration and login functionality. The problem arises when a new user logs in, as I must refresh the page to get the current user information. I am currently using interpolation on the ...

NestJS is having trouble importing generated types from the Prisma client

When working with Prisma in conjunction with NestJs, I encountered an issue after defining my model and generating it using npx prisma generate. Upon importing the generated type, I can easily infer its structure: import { FulfilmentReport, FulfilmentRepor ...

Encountering a Typescript error while attempting to extract state from a History object containing an enum property

My enum structure is as follows: enum values { first, second, } Within my component, I handle the history object like this: const { push, location: { state = {} } } = useHistory(); Additionally, in the same component within a useEffect hook, I have ...

What is the equivalent bundle size limit in bytes for the limitBytes value in the Rollup plugin analyzer?

I recently integrated the rollup plugin analyzer into my ReactJS project. While exploring the "CI usage example" section in the documentation, I noticed a variable named const limitBytes = 1e6 const limitBytes = 1e6 const onAnalysis = ({ bundleSize }) =& ...

Having trouble with ng-select focus in Angular 5?

I've been attempting to implement focus on ng-select within Angular 5, but unfortunately, it doesn't seem to be working as expected. Currently, I am utilizing ng2-select for my project. Is there a specific method for implementing focus on ng-se ...

Scroll automatically to the last div whenever a button is clicked using React and Typescript

I'm currently working on an application being developed in React + Typescript. I am trying to implement auto-scroll functionality to the last div within a parent div where child divs are dynamically added based on data from an array of objects. The da ...

What steps can be taken to effectively build a test suite architecture using Jest?

After exploring all the available resources on the Jest documentation website, including various guides and examples, I have yet to find a solution to my specific question. I am in search of a methodology that would enable me to individually run test case ...

The component is rendering properly, however the router-outlet in Angular seems to be getting overlooked

I've set up a router-outlet in app.component.html, admin.component.html, and manage-users.component.html. However, I'm facing an issue where the router-outlet in manage-users.component.html is not showing anything when I navigate to http://localh ...

What is the most efficient method for sharing types within an extensive TypeScript project?

I'm currently developing a complex React application using TypeScript. I have numerous common types defined in separate files, and I find myself importing them every time I need to use them. While this approach is functional, it results in a large num ...

Distinguishing between TypeScript versions 2.0.x and 2.1.x using type definitions and filtering with a switch/case statement

@ngrx/store (an observable redux implementation for angular (2) ) utilizes a specific pattern to assign the correct type to a reducer. Check out the actual code here. export const ActionTypes = { FOO: type('foo'), BAR: type('bar&apos ...

Angular: Deciding Between Utilizing Boolean @Input and Attribute @Directive - What's the Best Approach?

My goal with Angular is to create a "directive" that can add functionality to my component, specifically adding a myPortlet with a close button when using the directive myHasCloseButton. <myPortlet myHasCloseButton>...</myPortlet> In explori ...

Enhanced hierarchical organization of trees

I came across this code snippet: class Category { constructor( readonly _title: string, ) { } get title() { return this._title } } const categories = { get pets() { const pets = new Category('Pets') return { ge ...

What suggestions do you have for standout TypeScript presentations?

Currently, I am enrolled in a TypeScript program and keen to experiment with some demonstrations to enhance my comprehension of the topics. Could anyone recommend showcases ranging from beginner level to more advanced stages? Appreciate your assistance. ...

What's the best way to track changes in multiple form fields simultaneously in Angular?

Situation I have a form with 8 fields, but I want to monitor changes in just three of them to apply the same function. I don't want to set up individual subscriptions for each field like this: this.headerForm.get('start').valueChanges.subsc ...

Guide on automatically populating a value in an input field

My form includes a hook that automatically populates inputs based on the zip code entered by the user, filling in their address details seamlessly. Unfortunately, this auto-fill feature triggers a re-render of the component, causing the modal to open and ...

What steps should I take to import a module with type definitions? (receiving error TS2656: ...not a module)

I am currently working on enhancing the type definitions for a simple npm module called emitter20. The source code of this module spans 20 lines and looks like this: module.exports = function() { var subscribers = [] return { on: function (eventNa ...

What makes TS unsafe when using unary arithmetic operations, while remaining safe in binary operations?

When it comes to arithmetic, there is a certain truth that holds: if 'a' is any positive real number, then: -a = a*(-1) The Typescript compiler appears to have trouble reproducing arithmetic rules in a type-safe manner. For example: (I) Workin ...

Determine whether a many-to-many relationship involves a specific entity

I am currently working on developing an API for managing surveys. One challenge I'm facing is determining whether a specific user has moderation privileges for a particular survey. A many-to-many relationship has been set up between the two entities. ...

Troubleshooting the unexpected behavior of the shareReply() method in Angular version 15

Utilizing the same API across 3 components can lead to duplicate HTTP calls. To prevent this, I decided to cache the response using shareReply() from RxJs so it can be reused wherever needed. Here's how I implemented it: api-service.ts getUsers(): Ob ...