What is the process for creating a map in which the value type is determined by the key type?

Is it feasible to formulate a Map<K, V> in a manner where the type of the value relies on the type of the key, without explicitly specifying the key's type upon initializing the map?

For instance:

abstract class BaseA { a() {} }
class ConcreteA1 extends BaseA { a1() {} }
class ConcreteA2 extends BaseA { a2() {} }
abstract class BaseB { b() {} }
class ConcreteB extends BaseB { b1() {} }

const map = new Map<???, ???>();
map.set(BaseA, ConcreteA1); // should work
map.set(BaseA, ConcreteA2); // should work
map.set(BaseB, ConcreteB);  // should work

map.set(BaseA, ConcreteB); // should result in an error

Extra challenge: can this be flagged as an error too?

Considering BaseA is abstract, it should raise an error if attempted, since BaseA doesn't meet the criteria of new (...args: any[]) => BaseA, given that it cannot be instantiated.

map.set(BaseA, BaseA); // should trigger an error as an additional challenge

UPDATE:

I aim to maintain a map of dependency implementations, with the values representing concrete implementations (extending) the key (base type).

Answer №1

The TypeScript typings for Map exclusively cater to Map<K, V>, where K represents the key type, V is for the value type, without any additional connection between them. Hence, using the generic Map as-is won't enable the TypeScript compiler to impose extra constraints.

Based on the context of your inquiry, it appears that you require the key to be a (potentially abstract) constructor type and the corresponding value to be a definitively concrete constructor of a compatible type. Given that your example code solely utilizes the set() method, the minimal typings needed to align with your specifications are:

interface MySpecialMap {
  set<T extends object>(
    k: abstract new (...args: any) => T, 
    v: new (...args: any) => T
  ): this;
}
interface MySpecialMapConstructor {
  new(): MySpecialMap;
}
const MySpecialMap: MySpecialMapConstructor = Map;

In this representation, I've designated at runtime the MySpecialMap constructor to mirror the Map constructor but assigned it the type MySpecialMapConstructor, responsible for creating instances of MySpecialMap. The MySpecialMap interface hosts just one method - set(), which is generic in terms of the base class instances under type T. The k parameter signifies a potential abstract constructor associated with type T instances, while v denotes an obligatory concrete constructor for T instances.

Lets conduct a practical demonstration to validate its functionality:

abstract class BaseA { a() { } }
class ConcreteA1 extends BaseA { a1() { } }
class ConcreteA2 extends BaseA { a2() { } }
abstract class BaseB { b() { } }
class ConcreteB extends BaseB { b1() { } }

const map = new MySpecialMap();
map.set(BaseA, ConcreteA1); // successful
map.set(BaseA, ConcreteA2); // successful
map.set(BaseB, ConcreteB);  // successful
map.set(BaseA, ConcreteB); // error
// Property 'a' is missing in type 'ConcreteB' but required in type 'BaseA'
map.set(BaseA, BaseA); // error
// Cannot assign an abstract constructor type to a non-abstract constructor type.

All seems well!

Subsequently, you may want to incorporate similar typings for get() or any other requisite methods. Alternatively, you can adjust the call signature for set() to specify zero-arg constructors or any other restrictions you wish to enforce. This initial setup should lay a solid foundation for you to proceed.

Interactive code demo link

Answer №2

TypeScript utilizes a Structural Type System, which means that it compares types based on their members rather than their names. This is different from nominal type systems where two types with the same structure cannot be assigned to each other. (Learn more here)

To overcome this limitation, you can make your base class unique by adding a private member like #type. By doing this, you can create a new type that restricts certain mappings between classes. Here's an example:

abstract class BaseA {
    #type = BaseA.name;
}
class ConcreteA extends BaseA {
}
abstract class BaseB {}
class ConcreteB extends BaseB {
}

type MyMap<T, S> = S extends T ? Map<T, S> : never; 
const map1: MyMap<BaseA, ConcreteA> = new Map();
//   ^?
const map2: MyMap<BaseA, ConcreteB> = new Map();
//   ^?

Check out this playground

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

Tips for preventing keyboard events from being inherited by all pages in the stack in Ionic framework

In my Ionic 3 app, I have a specific page called Page1 that requires customized keyboard handling. Here is how I implemented it on Page1: @Component({ ... host: { '(document:keydown)': 'handleKeyboardEvents($event)' } }) expo ...

Instructions on how to sign up for a worldwide technique that is known as

I have a file called globalvars.ts where I added a global method. How can I subscribe to this method in the ts page where it is being called? globalvars.ts; httpgetmethod(url:string) { var veri; var headers = new Headers(); headers.append(' ...

Conceal the HTML element within a subscription

Currently, I am utilizing Angular and have a checkbox that I need to toggle visibility based on the response of an API call subscription. The issue lies in a delay when trying to hide the checkbox (as it is initially set to visible). My assumption is that ...

What is the method for verifying that one type extends another in the TypeScript compiler API?

In the process of building a tool (partly to test its functionality), I am developing a way to condense a set of TypeScript definitions into a clean d.ts file, while ignoring unnecessary helper types used for reshaping data. This approach is proving quite ...

Can you set up a mechanism to receive notifications for changes in an array variable in Angular?

I'm exploring methods to delay an HTTP request until the user stops interacting. I am considering using the debounceTime() operator from RxJs, but I need this to be triggered by changes in an array that I have defined. Here is the scenario: export c ...

The file node_modules/angular2-qrscanner/angular2-qrscanner.d.ts has been detected as version 4, while version 3 was expected. Resolving symbol

We're encountering a Metadata error that is causing obstacles in our deployment process. This issue is preventing the execution of ng build. Below, you will find the configuration details along with the complete error trace. ERROR in Error: Metadata ...

Restricting number input value in Vue using TypeScript

I have a component that looks like this: <input class="number-input py-1 primary--text font-weight-regular" :ref="'number-input-' + title" @keypress="onKeyPressed" :disabled="disabled& ...

Identifying the specific @Input that has changed in ngOnChanges

I am currently utilizing Angular 2. At the moment, I have two @input variables named aa and bb. My objective is: When aa changes, perform a specific action. When bb changes, execute a different action. How can I identify which @Input has changed within ...

Intellisense from @reduxjs/toolkit is not showing up in my VS Code editor

My experience with vscode is that intellisense does not recognize @reduxjs/toolkit, even though the code itself is functioning correctly. I have already installed the ES7+ React/Redux/React-Native snippets extension from here. Here are a couple of issues ...

Storing references to the DOM elements external to the rendering component

Just diving into the world of Electron + Typescript, so please bear with me. Currently, I'm experimenting with what can be achieved within Electron. Issue: My goal is to manipulate DOM elements outside of the renderer. I pass a button as a parameter ...

unable to successfully complete parameter in angular 2

After receiving data from the API, I am using the subscribe method to execute lines of code. Below is the code snippet: this.attRecService.getAgendaData(moment(this.viewDate).format('YYYY-MM')).subscribe( resp => { this.ag ...

The Next.js template generated using "npx create-react-app ..." is unable to start on Netlify

My project consists solely of the "npx create-react-app ..." output. To recreate it, simply run "npx create-react-app [project name]" in your terminal, replacing [project name] with your desired project name. Attempting to deploy it on Netlify Sites like ...

Regex struggles to identify words containing foreign characters

Here is a method I have created to check if a user-input term matches any blacklisted terms: static checkAgainstBlacklist(blacklistTerms, term) { return blacklistTerms.some(word => (new RegExp(`\\b${word}\\b`, 'i&ap ...

NextJS - The server attempted to execute the find() function, which is only available on the client side

When attempting to utilize the .find method within the server component, I encounter an error. export async function TransactionList() { const transactions = await fetch('/transactions'); return ( <ul> {transactions.m ...

Is it possible to generate a Date object from a predetermined string in typescript?

I have a string with values separated by commas, and I'm trying to create a Date object from it. It seems like this is not doable -- can someone verify this and provide a solution if possible? This code snippet doesn't work : let dateString=&a ...

Obtain form data as an object in Vue when passed in as a slot

Currently, I am working on developing a wizard tool that allows users to create their own wizards in the following format: <wiz> <form> <page> <label /> <input /> </page> <page> <label /> ...

Issues with Angular2 causing function to not run as expected

After clicking a button to trigger createPlaylist(), the function fails to execute asd(). I attempted combining everything into one function, but still encountered the same issue. The console.log(resp) statement never logs anything. What could be causing ...

The 'jsx' property in tsconfig.json being overridden by Next.js and TypeScript

Is there a way to prevent NextJS from changing the 'jsx' property in my tsconfig.json file from 'react' to 'preserve' when running the development server? This is how my tsconfig.json file looks: "compilerOptions": { " ...

Sending VSCode to external functions

My primary entrypoint containing the activate() function is: extension.ts import * as vscode from "vscode"; import { subscribe } from "./eventListeners.ts"; export function activate(context: vscode.ExtensionContext) { vscode.command ...

The data structure of '(string | undefined)[]' cannot be matched with type '[string | undefined]'

I've been working on a TypeScript project and I've encountered the ts(2322) error as my current challenge. Here's a snippet of my code: import { BASE_URL, AIRTABLE_BASE_ID, AIRTABLE_TABLE_STUDENT, AIRTABLE_TABLE_CLASSES, API_KEY, ...