Implementing a Map in Typescript that includes a generic type in the value

Here is a code snippet I am working with:

class A<T> {
  constructor(public value: T) {}
}

const map = new Map();
map.set('a', new A('a'));
map.set('b', new A(1));

const a = map.get('a');
const b = map.get('b');

Currently, the variables a and b are inferred as any. If I modify it to this:

const map = new Map<string, A<????>>(); 

I want to specify the generic type of A, but then I lose the typing for the value property. Is there a way to achieve both?

const a = map.get('a') // should be inferred as A<string>
const b = map.get('b') // should be inferred as A<number>

Answer №1

TypeScript's type system works on the premise that values must have a fixed type that remains constant over time. For example, if you declare let a: string = "hey";, you are informing the compiler that a is and will always be of type string. Attempting to later assign a = 4; will result in a compile-time error. Similarly, when you define let b = "hey"; without explicitly specifying the type, the compiler will infer it as string, hence writing b = 4; afterwards will trigger an error. To allow a variable to hold different types at different times, you should annotate it as string | number upon declaration, like so: let c: string | number = "hey";. Subsequently, assigning c = 4; would be acceptable.

In TypeScript, when working with objects, you need to predefine the property types during declaration. Therefore, the following code snippet will produce errors:

let o = {}; // type {}
o.a = new A('a'); // error, {} has no "a" property
o.b = new A(1); // error, {} has no "b" property

On the other hand, the subsequent code block will execute successfully:

let o2 = { a: new A('a'), b: new A(1) }; // okay
// let o2: { a: A<string>; b: A<number>; }

An advantage of TypeScript is its control flow type analysis feature, where the compiler temporarily narrows the type of a value based on specific conditions. For instance, after initializing let c: string | number = "hey", calling c.toUpperCase() won't raise an error because the type of c transitions from string | number to

string</code. However, without this analysis, you'd require a type assertion such as <code>(c as string).toUpperCase()
. It's crucial to note that narrowing a value's type via control flow type analysis can only restrict it to a subtype or revert back to the original annotated/inferred type upon reassignment. While this technique enables adding properties to objects dynamically, it doesn't facilitate altering existing property types or deleting properties outright. If deletion is necessary, newly added properties should be optional.

In TypeScript 3.7, assertion functions were introduced to customize the narrowing process for function arguments based on control flow logic. As demonstrated through the custom setProp function, which appends optional properties to an object, you have flexibility in extending objects while maintaining type safety:

function setProp<O extends object, K extends PropertyKey, V>(
    obj: Partial<O>,
    key: Exclude<K, keyof O>, value: V
): asserts obj is Partial<O & Record<K, V>> {
    (obj as any)[key] = value as any;
}

The usage example clarifies how map can be progressively updated using setProp to insure type consistency:

const map = {}
map.a = new A('a'); // error, cannot perform this
// but the following statement is acceptable:
setProp(map, 'a', new A('a'));
// once set, you can reallocate it to the same type
map.a = new A('b');

setProp(map, 'b', new A(1));
setProp(map, 'c', "a string");

To access and delete properties, typical property access methods work provided you check for undefined beforehand due to their optional nature:

map.a && console.log(map.a.value); // b
map.b && console.log(map.b.value); // 1
delete map.b; // okay        
map.c && console.log(map.c.toUpperCase()); // A STRING 

Ultimately, defining types upfront during object declaration rather than relying solely on control-flow type narrowing enhances overall robustness. Although convenient, control-flow-based narrowing should complement explicit annotations for optimal code clarity and reliability.


If opting for a Map over a standard object, consider crafting customized typings for Map to accommodate varying value types more efficiently. Check out the Playground Link mentioned below for a comprehensive implementation along with the aforementioned code segments.

I trust these insights provide valuable guidance. Best of luck!

Playground Link

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

`Property cannot be redefined: __internal__deprecationWarning` detected in a Shopify Hydrogen development project

Recently, while working on my Shopify Hydrogen project using Remix and Typescript, I encountered a sudden error when running npm run dev. Everything was functioning perfectly just 5 hours ago, but after returning from dinner, the app refuses to launch. ╭ ...

What is the best method to locate an element<T> within an Array[T] when <T> is an enum?

I've recently started learning TypeScript and exploring its functionalities. I could use some assistance in deepening my understanding. Within our angular2 application, we have defined an enum for privileges as follows: export enum UserPrivileges{ ...

What distinguishes between the methods of detecting falsy and truthy values?

While working with JavaScript / Typescript, I often find myself needing to verify if a length exists or if a value is true or false. So, the main query arises: are there any differences in performance or behavior when checking like this... const data = [ ...

Exploring the use of a customizable decorator in Typescript for improved typing

Currently, I am in the process of creating TypeScript typings for a JavaScript library. One specific requirement is to define an optional callable decorator: @model class User {} @model() class User {} @model('User') class User {} I attempted ...

Utilize an enum to serve as a blueprint for generating a fresh object?

I've defined an enum as shown below: export enum TableViewTypes { user = 'users', pitching = 'pitching', milestones = 'milestones', mediaList = 'mediaList', contacts = 'contacts' } ...

Explaining the distinction between include and rootDir in tsconfig.json

According to the information provided, include defines an array of filenames or patterns that are to be included in the program during the compilation process. On the other hand, rootDir specifies the path to the folder containing the source code of the ap ...

Steps for converting an observable http request into a promise request

Here is a link to my service file: This is the TypeScript file for my components: And these are the response models: I am currently facing difficulties with displaying asynchronously fetched data in my component's TypeScript file using an Observabl ...

Using the `forwardRef` type in TypeScript with JSX dot notation

Currently, I am exploring the different types for Dot Notation components using forwardRef. I came across an example that showcases how dot notation is used without forwardRef: https://codesandbox.io/s/stpkm This example perfectly captures what I want to ...

The process of removing and appending a child element using WebDriverIO

I am trying to use browser.execute in WebDriverIO to remove a child element from a parent element and then append it back later. However, I keep receiving the error message "stale element reference: stale element not found". It is puzzling because keepin ...

Guide to configuring an Appium-Webdriver.io project for compiling TypeScript files: [ ISSUE @wdio/cli:launcher: No test files detected, program exiting with error ]

I have decided to transition my Appium-Javascript boilerplate project into a typescript project. I am opting for the typed-configuration as it is officially recommended and have followed the steps outlined in the documentation. Here is an overview of the ...

"I am looking for a way to incorporate animation into my Angular application when the data changes. Specifically, I am interested in adding animation effects to

Whenever I click on the left or right button, the data should come with animation. However, it did not work for me. I tried adding some void animation in Angular and placed a trigger on my HTML element. The animation worked when the page was refreshed, bu ...

The specified property 'slug' is not found within the designated type 'ParsedUrlQuery | undefined'

I am faced with an issue in my code where I am attempting to retrieve the path of my page within the getServerSideProps function. However, I have encountered a problem as the type of params is currently an object. How can I convert this object into a stri ...

What is the best way to assign the value of "this" to a variable within a component using Angular 2 and TypeScript?

In my component, I have the following setup: constructor() { this.something = "Hello"; } document.addEventListener('click', doSomething()); function doSomething(e) { console.log(this.something) // this is undefined } I am struggling to a ...

Transmit a form containing a downloaded file through an HTTP request

I am facing an issue with sending an email form and an input file to my server. Despite no errors in the console, I can't seem to upload the file correctly as an attachment in the email. post(f: NgForm) { const email = f.value; const headers = ...

Different ways to separate an axios call into a distinct method with vuex and typescript

I have been working on organizing my code in Vuex actions to improve readability and efficiency. Specifically, I want to extract the axios call into its own method, but I haven't been successful so far. Below is a snippet of my code: async updateProf ...

The error message "Property <property> is not recognized on the type 'jQueryStatic<HTMLElement>'" is indicating an issue with accessing a specific property within the TypeScript codebase that utilizes Angular CLI, NPM,

My Development Environment I am utilizing Angular, Angular CLI, NPM, and Typescript in my web application development. Within one of my components, I require the use of jQuery to initialize a jQuery plugin. In this particular case, the plugin in question ...

Issue: Unable to link with 'dataSource' as it is not a recognized feature of 'mat-tree'

Upon following the example provided at https://material.angular.io/components/tree/overview, I encountered an error when trying to implement it as described. The specific error message is: Can't bind to 'dataSource' since it isn't a kn ...

Is it possible to have a button within a table that, when clicked, opens a card overlaying the entire table?

I'm having an issue with a table and card setup. When I click the button in the table, the card that appears only covers part of the table. I want it to cover the entire table area based on the content inside the card. How can I make this happen? I&a ...

There is no way to convert a strongly typed object into an observable object using MobX

I have been utilizing the MobX library in conjunction with ReactJS, and it has integrated quite smoothly. Currently, I am working with an observable array structured as follows: @observable items = []; When I add an object in the following manner, everyt ...

Running a TypeScript file on Heroku Scheduler - A step-by-step guide

I have set up a TypeScript server on Heroku and am attempting to schedule a recurring job to run hourly. The application itself functions smoothly and serves all the necessary data, but I encounter failures when trying to execute a job using "Heroku Schedu ...