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

Exploring the World of Angular: Abstracts and Data Transformations

Facing a perplexing issue with a component that is based on an abstract class, or at least that's my assumption. There are multiple sibling components rendered using *ngFor based on an array length. These siblings, derived from an abstract class, rec ...

Tips for resolving TS7022 issue within Vue Composition API

I encountered an issue while attempting to import a component in my file that generated the following error message: TS7022: 'default' implicitly has type 'any' because it does not have a type annotation and is referenced directly or in ...

Send an API request using an Angular interceptor before making another call

Within my application, there are multiple forms that generate JSON objects with varying structures, including nested objects and arrays at different levels. These forms also support file uploads, storing URLs for downloading, names, and other information w ...

What is the recommended data type for Material UI Icons when being passed as props?

What specific type should I use when passing Material UI Icons as props to a component? import {OverridableComponent} from "@mui/material/OverridableComponent"; import {SvgIconTypeMap} from "@mui/material"; interface IconButtonProps { ...

Clear out chosen elements from Angular Material's mat-selection-list

Looking for a way to delete selected items from an Angular Material list, I attempted to subtract the array of selected items from the initial array (uncertain if this is the correct approach). The challenge I face is figuring out how to pass the array of ...

Creating a welcome screen that is displayed only the first time the app is opened

Within my application, I envisioned a startup/welcome page that users would encounter when the app is launched. They could then proceed by clicking a button to navigate to the login screen and continue using the app. Subsequently, each time the user revisi ...

Add a new child component template with each click using the onclick event in Angular

Is there a way to dynamically add a child component with each button click event? Here is the HTML code for the button: <button type="button" class="btn btn-success btn-sm btn-add-phone" (click)="addfield()"><span class="fa fa-plus"></span ...

Using TypeScript to pass a callback function to labelFormatter in the legend of a Highcharts chart

I am currently experimenting with integrating HighCharts into an Angular2 project using TypeScript. My goal is to customize the appearance of the legend text, adding an image next to it. I've found that HighCharts provides a labelFormatter property w ...

MUI version 5 with styled-components and ListItemButton: The specified property 'to'/'component' is not recognized

While transitioning from MUI v4 to v5, I encountered a hurdle: in v4, I utilized makeStyles(), but now aim to fully switch to styled(). Unfortunately, I am struggling to get Typescript to recognize a styled(ListItemButton)(...) with to= and component= attr ...

In order to showcase the data from the second JSON by using the unique identifier

SCENARIO: I currently have two JSON files named contacts and workers: contacts [ { "name": "Jhon Doe", "gender": "Male", "workers": [ "e39f9302-77b3-4c52-a858-adb67651ce86", "38688c41-8fda-41d7-b0f5-c37dce3f5374" ] }, { "name": "Peter ...

What is the best way to remove linear-gradient effects applied by a dark mode theme?

Why does MUI add random gradients to components, like in dark mode? Is there a way to disable this feature because it doesn't match the exact color I expected for my custom theme... My Theme Options export const themeOptions: ThemeOptions = { palette ...

Is the graphql codegen accurately generating the types?

I'm in the process of developing a basic Next.js application with TypeScript by integrating Strapi as a headless CMS. The main goal is to use Strapi and GraphQL, along with TypeScript, to display content on the Next.js app. Within Strapi, there is a ...

Does Angular 2/4 actually distinguish between cases?

I have a question about Angular and its case sensitivity. I encountered an issue with my code recently where using ngfor instead of ngFor caused it to not work properly. After fixing this, everything functioned as expected. Another discrepancy I noticed w ...

Merge two input fields into one to send data to the backend

I have created two input fields, one for selecting a date and the other for entering a time. Before submitting the form to the backend, I need to combine these two inputs into one variable. For example, <input type="text" name="myDate"> and <input ...

The React Native Flatlist encountered an error due to a mismatch in function overloads

I'm currently working on a React Native app using Typescript, and I've encountered an issue with the Flatlist renderItem function. As someone who is new to both Typescript and React Native, I received the following error message: No overload ma ...

Angular 5 and Bootstrap card with enhanced double-click functionality

I want to design a Bootstrap card that can trigger two of my custom methods. <div (click)="TEST1()" class="card" style="width: 18rem;"> <div class="card-body"> <h5 class="card-title">Card title</h5> <button (click)="(T ...

TS - Custom API hook for making multiple API requests - incompatible type with 'IUseApiHook'

What is my objective? I aim to develop a versatile function capable of handling any type of API request for a frontend application. Essentially, I want to add some flair. Issue at hand? I find myself overwhelmed and in need of a fresh perspective to revi ...

Convert string to integer value

Is it possible to convert a literal string type (not runtime value) to its corresponding numerical type, for example '1' to 1? I am looking to restrict a variable to only being a key of an object (assuming the type of obj is precise since TypeSc ...

Testing the integration of socket.io with Angular through unit tests

Currently, I am in the process of unit testing an angular service within my application that is responsible for creating a socket.io client. The structure of my service can be seen below: export class SocketService { private name: string; private host ...

Eliminate the need to input the complete URL every time when making server calls

Currently, my springbok application has a React-Typescript frontend that is functioning well. I am using the request-promise library to make requests like this: get('http://localhost:8080/api/items/allItems', {json: true}). However, I would like ...