Can you determine the base class type based on the derived class?

I've encountered a typings issue while working on a new feature for my TypeScript library. Let's dive into the problem with the following example:

class Base { ... }

class User extends Base { ... }
class Product extends Base { ... }

class CompletelyUnrelated { ... }

function someFunction<TModel>(source: TModel, base: {what_to_type_here}) { ... };

// Desired outcome
someFunction(User, Base); // works
someFunction(Product, Base); // works

someFunction(CompletelyUnrelated, Base); // error
someFunction(User, Product); // error
someFunction(Product, User); // error
someFunction(User, CompletelyUnrelated); // error

My goal is for someFunction() to only accept User and Base as its arguments due to the inheritance relationship between them. The same applies for Product and Base. Is there a feasible solution for this requirement? Is there any approach that aligns with my objective?

I've attempted:

interface Constructible<TModel> { // an interface to denote a new-able type (included here for context)
   new(...args: any[]): TModel;
}

type BaseConstructible<TModel> = TModel extends infer TBase ? Constructible<TBase> : any;

However, the outcome always yields Constructible<TModel>:

type Test = BaseConstructible<User>; // returns Constructible<User>

Any assistance or direction on this matter would be greatly appreciated. Thank you

UPDATE: Acknowledging @jcalz's point about TypeScript having a structural type system, I am well aware of this fact. While @jcalz's response is accurate, it doesn't completely address my specific requirement. Consider this scenario:

function anotherFunction<TBase , TModel extends TBase>(source: Constructible<TModel>) {
   return (base: Constructible<TBase>) => {
      // function body
   }
}

anotherFunction(User)(Product); // no error

TypeScript struggles to infer TBase from TModel extends TBase. Is there a way to make TypeScript infer TBase in this context?

Answer №1

Let's start by discussing the type system of TypeScript, which is structural and not nominal. This means that if two types have the same structure, they are considered the same type, even if they have different declarations. This is different from nominal typing found in languages like Java, C++, or C#, where two classes with different declarations are considered different types.

For instance, in TypeScript, if you have two empty classes, they will be treated as the same type as an empty interface. To make two classes distinct, they must have different members or use private or protected members for more nominal-like behavior.

Below are some classes with properties to ensure that Base, User, Product, and CompletelyUnrelated are related as intended:

class Base {
  base = "base"
}

class User extends Base {
  user = "user"
}

class Product extends Base {
  product = "product"
}

class CompletelyUnrelated {
  completelyUnrelated = "whatevz";
}

Now, let's define the type signature for the function someFunction(). We'll make it generic with two types, T and U. T represents the instance type of the base constructor, and U represents the instance type of the source constructor. U is constrained to extend T, and both parameters are constructors.

function someFunction<T, U extends T>(
  source: new (...args: any) => U,
  base: new (...args: any) => T
) { return null! };

Testing the function:

someFunction(User, Base); // okay
someFunction(Product, Base); // okay

someFunction(CompletelyUnrelated, Base); // error
someFunction(User, Product); // error
someFunction(Product, User); // error
someFunction(User, CompletelyUnrelated); // error

Great! The function behaves as expected, passing where it should and throwing errors where types are mismatched. Good luck with your TypeScript endeavors!


UPDATE:

If you want to split the someFunction() into anotherFunction() as a curried version, we'll need to do some more type manipulation to make it work. The issue is that the return type of anotherFunction() needs to allow for a supertype of the class instance type from the constructor. This requires expressing the type as a lower bound generic constraint, similar to the upper bound we used before.

Although TypeScript doesn't have a T super U lower bound constraint, we can achieve similar behavior using conditional types. This allows for a check to determine if U extends T and adjust the type accordingly.

function anotherFunction<U>(source: new (...args: any) => U) {
  return <T extends ([U] extends [T] ? unknown : never)>(base: new (...args: any) => T) => {
  }
}

Testing the new function:

anotherFunction(User)(Base); // okay
anotherFunction(Product)(Base); // okay
anotherFunction(Base)(Product); // error!
anotherFunction(CompletelyUnrelated)(Base); // error!
anotherFunction(User)(Product); // error!
anotherFunction(Product)(User); // error!
anotherFunction(User)(CompletelyUnrelated); // error!

The function behaves as intended, providing errors when types are mismatched. There are still some limitations, but overall it works well. Good luck with your TypeScript journey!

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

Error message: Unable to retrieve `__WEBPACK_DEFAULT_EXPORT__` before initializing Firebase Admin in a nx and nextjs application

My current project involves a Typescript Nx + Next.js App integrated with Firebase (including Firebase Admin). In this codebase, I have defined a firebase admin util as shown below - // File ./utils/FirebaseAdmin.ts // import checkConfig from './check ...

The interface is incompatible with the constant material ui BoxProps['sx'] type

How can I make the interface work for type const material ui? I tried to register an interface for sx here, but it keeps giving me an error. import { BoxProps } from '@mui/material'; interface CustomProps { sx: BoxProps['sx&apo ...

What is the process for importing components from a Stencil library into a React application?

After successfully creating: a stencilJS component library named "my-lib" using the "npm init stencil" wizard and an Ionic React app using "ionic start myApp tabs" I am now trying to incorporate the default "my-component," aka MyComponent from my-lib. H ...

"Upon invoking the services provider in Ionic 2, an unexpected undefined value was

I encountered an issue while setting a value in the constructor of my page's constructor class. I made a call to the provider to fetch data. Within the service call, I was able to retrieve the data successfully. However, when I tried to access my vari ...

Encountering a 'No overload matches this call.' error when using ApexCharts with Typescript and ReactJS

As a newcomer to Typescript, I am gradually familiarizing myself with this powerful tool. After fetching the OHLCV data from coinpaprika and passing it to ApexCharts, I encountered an issue while trying to map the raw data: ERROR in src/routes/Chart.tsx:3 ...

What is the process for passing information to a nested component structure with parent-child-child relationships?

I am facing an issue with three nested components (C1, C2, C3) where C2 is called within C1 and C3 is called within C2. My goal is to pass data from C1 to C3 using property binding. In the template of C1, I successfully bound a variable that I can access ...

Adding images to your SVG using Bobril is a simple process that can add visual

I have been attempting to insert an image into an SVG using Bobril, but the following code is not functioning as expected: { tag: 'svg', children: { tag: 'image', attrs: { 'xlink:href': &ap ...

Having trouble getting ng-click to function properly in TypeScript

I've been struggling to execute a function within a click function on my HTML page. I have added all the TypeScript definition files from NuGet, but something seems to be going wrong as my Click Function is not functioning properly. Strangely, there a ...

Trigger an Angular2 component function from an HTML element by simply clicking a button

I'm just starting out with TypeScript and Angular2 and encountering an issue when trying to call a component function by clicking on an HTML button. When I use the **onclick="locateHotelOnMap()"** attribute on the HTML button element, I receive this ...

a callback may seem like it's not a function, but in reality, it is

This question might seem simple, but I'm struggling to grasp the concept of callbacks, especially in NodeJS. My issue arises when trying to retrieve data from MySQL, something that is usually straightforward in most programming languages: In my rout ...

Oops! Looks like you forgot to provide a value for the form control named <name>. Make sure to fill

I have encountered an issue with a nested operation. The process involves removing an offer and then saving it again as a repeating one. However, the problem arises when I try to use patchValue() on the item in offerList[index] after saving the repeating ...

The generics in TypeScript for Parameters<URLS[T]> and the corresponding URLS[T] are not compatible

In the code below, I am encountering a type error: const urls = { inviteNewUser: ({teamId, intent = 'invite'}: {teamId: string; intent?: Intent}) => `/magic-link?intent=${intent}&teamId=${teamId}`, resetPassword: ({intent = 'r ...

Testing exception - unstable FlushDiscreteUpdates

I am currently working on a test using Jest and react-testing-library for a .tsx test file written in TypeScript: import React from "react"; import { Provider } from 'react-redux'; import { render, screen } from "@testing-library/r ...

Accessing User Session with NextAuth in Application Router API Route

Utilizing NextAuth in conjunction with Next.js's App Router, I have established an API route within my /app/api directory. Despite my efforts, I am encountering difficulties retrieving the session associated with the incoming request. Below is the sn ...

Warning: An alert has been triggered while generating files using create-react-app

npm WARN <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e896999d9c81849ba8dbccd1ded8d4cfdcd0c59bc6cbca">[email protected]</a> requires a peer of typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= ...

What sets aws-cdk-lib apart from @aws-cdk/core, @aws-cdk/aws-iam, and others?

There seems to be a variety of examples out there using the AWS CDK, with some referencing aws-cdk-lib and others using @aws-cdk/core. Can someone clarify the distinction between these two and provide guidance on when to use one over the other? ...

String Compression - Number of Elements

Suppose I define a specific type: type SomeType = 'a' | 'b' | 'c' Is there a TypeScript function available that can calculate the number of unique values a variable of type SomeType can hold? assertEq(countUniqueValues(SomeTy ...

Contrast the differences between arrays and inserting data into specific index positions

In this scenario, I have two arrays structured as follows: arr1=[{room_no:1,bed_no:'1A'}, {room_no:1,bed_no:'1B'}, {room_no:2,bed_no:'2A'}, {room_no:3,bed_no:'3A'}, {room_no:3,bed_no:'3B ...

TypeScript's TypeGuard wandering aimlessly within the enumerator

I'm puzzled by the fact that filter.formatter (in the penultimate line) is showing as undefined even though I have already confirmed its existence: type Filter = { formatter?: { index: number, func: (value: string) => void ...

Troubleshooting: NextJS Typescript getInitialProps returning null value

I am currently working with NextJS 'latest' and TypeScript to extract the token from the URL, but I am encountering an issue where it returns undefined. To achieve this, I am utilizing the getInitialProps method. The URL in question looks like th ...