Utilize the type name as the indexer for the interface

Issue with Abstract Concepts

My challenge involves relating two distinct groups of types to one another.

// Group A
interface Hello { ... }
interface Foo { ... }

// Group B
interface World { ... }
interface Bar { ... }

To tackle this problem, I am creating a third interface that serves as a mapping from one type to another:

interface LookupMap {
  Hello: World;
  Foo: Bar;
}

The goal is to navigate from one type to another using a single interface as an index for the LookupMap (similar to how strings and numbers can be used for type lookups in indexed object literals):

type AltType<T> = LookupMap<T>;

const altHello: AltType<Hello> = ...; // intended to be of type World
const altFoo: AltType<Foo> = ...;     // intended to be of type Bar

However, this approach does not work as types cannot be used as indexers in this manner.


Real-Life Scenario

My aim is to enhance typing in Immutable.js.

Beneath this point, there is some complicated code. If you already have a solution, you may skip it...

Immutable objects offer a variety of useful functions. For instance, let's focus on adding typing to Map.get.

interface Immutalizer<MUTABLE_TYPE> extends Immutable.Map<keyof MUTABLE_TYPE, MUTABLE_TYPE[keyof MUTABLE_TYPE]> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: MUTABLE_TYPE[PROP_NAME]): MUTABLE_TYPE[PROP_NAME];
}

interface MyInterface {
  hello: boolean;
  world: string;
  foo: number;
  bar: symbol;
}

interface MyImmutableInterface extends Immutalizer<MyInterface> {};

const myObject: MyImmutableInterface = Immutable.fromJS(...);
myObject.get("hello"); // boolean
myObject.get("world"); // string
myObject.get("foo");   // number
myObject.get("bar");   // symbol

Furthermore, if some properties are complex objects, an additional type must be provided to Immutalizer for context:

interface Immutalizer<
  MUTABLE_TYPE,
  COMPLEX_OBJECT_KEYMAP extends { [PROP_NAME in keyof MUTABLE_TYPE]: any } = MUTABLE_TYPE
> extends Immutable.Map<
  keyof MUTABLE_TYPE,
  COMPLEX_OBJECT_KEYMAP[keyof MUTABLE_TYPE]
> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: COMPLEX_OBJECT_KEYMAP[PROP_NAME]): COMPLEX_OBJECT_KEYMAP[PROP_NAME];
}


interface Hello {
  foo: string;
  bar: World;
}

interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello, {
  foo: string;
  bar: ImmutableWorld;
}> {};

interface ImmutableWorld extends Immutalizer<World> {}; 

const myObject: ImmutableHello = Immutable.fromJS(...);
myObject.get("bar");          
myObject.get("foo");          
myObject.get("bar").get("a"); 
myObject.get("bar").get("b"); 

This process involves a lot of manual work, especially as the object tree deepens. I devised an alternative solution that is almost functional but not quite there yet:

type Primitive = string | number | boolean | symbol | String | Number | Boolean | Symbol;
type PrimitiveSwitch<PROP_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> =
  PROP_TYPE extends Primitive ?
  PROP_TYPE :
  IMMUTALIZER_MAP[PROP_TYPE]; // TYPE ERROR: Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.
interface ImmutalizerMap { [mutableType: string]: Immutalizer<any, this> }

interface Immutalizer<MUTABLE_TYPE, IMMUTALIZER_MAP extends ImmutalizerMap> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(
    prop: PROP_NAME,
    notSetValue?: PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>
  ): PrimitiveSwitch<MUTABLE_TYPE[PROP_NAME], IMMUTALIZER_MAP>;
}


export interface Hello {
  foo: string;
  bar: World;
}

export interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello, ImmutalizerMap> { }
interface ImmutableWorld extends Immutalizer<World, ImmutalizerMap> { }

interface MyImmutalizerMap {
  Hello: ImmutableHello;
  World: ImmutableWorld;
}

const hello: ImmutableHello = Immutable.fromJS(...);

hello.get("foo"); 
hello.get("bar"); 

The Immutalizer section is somewhat complex, but using it is now theoretically straightforward:

  • Maintain a mapping of all types and their associated immutable types.
  • Create immutable types using Immutalizer by passing in the base type and the relevant ImmutalizerMap it belongs to.
  • Immutalizer handles the rest, utilizing PrimitiveSwitch to determine if a type needs to be looked up in the ImmutalizerMap or not.

Despite these efforts, accessing IMMUTALIZER_MAP[PROP_TYPE] in PrimitiveSwitch triggers a type error:

Type 'PROP_TYPE' cannot be used to index type 'IMMUTALIZER_MAP'.


Inquiry

Is it possible to use an interface (name) as indexers in other interfaces? Are there alternative solutions for improving typing in Immutable objects?

Answer №1

Although not directly addressing the question about indexers, I managed to devise a more comprehensive approach to incorporating typing into Immutable.js using recursion:

type Primitive = string | number | boolean | symbol | String | Number | Boolean | Symbol;

type PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME extends keyof MUTABLE_TYPE> = MUTABLE_TYPE[PROP_NAME] extends Primitive ? MUTABLE_TYPE[PROP_NAME] : Immutalizer<MUTABLE_TYPE[PROP_NAME]>

interface Immutalizer<MUTABLE_TYPE> extends Immutable.Map<keyof MUTABLE_TYPE, Immutalizer<MUTABLE_TYPE[keyof MUTABLE_TYPE]> | MUTABLE_TYPE[keyof MUTABLE_TYPE]> {
  get<PROP_NAME extends keyof MUTABLE_TYPE>(prop: PROP_NAME, notSetValue?: PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME>): PrimitiveSwitch<MUTABLE_TYPE, PROP_NAME>
}

interface Hello {
  foo: string;
  bar: World;
}

interface World {
  a: number;
  b: symbol;
}

interface ImmutableHello extends Immutalizer<Hello> { };
interface ImmutableWorld extends Immutalizer<World> { };

let hello: ImmutableHello = Immutable.fromJS({});
let world: ImmutableWorld = hello.get("bar");

hello.get("bar").get("b"); // symbol
world.get("b");            // symbol

While the interfaces lose their names (hello.get("bar") now returns Immutalizer<World> instead of ImmutableWorld), the types remain compatible.

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

Why is TS1005 triggered for Redux Action Interface with an expectation of '=>'?

I'm finding it difficult to identify what's causing this issue, as shown in the esLint error from Typescript displayed in the screenshot below: https://i.stack.imgur.com/pPZa7.png Below is the complete code, which consists of actions for redux. ...

The method of implementing an index signature within TypeScript

I'm currently tackling the challenge of using reduce in Typescript to calculate the total count of incoming messages. My struggle lies in understanding how to incorporate an index signature into my code. The error message that keeps popping up states: ...

What is the best way to retrieve the dataset object from a chart object using chart.js in typescript?

Currently, I am facing a challenge in creating a new custom plugin for chart.js. Specifically, I am encountering a type error while attempting to retrieve the dataset option from the chart object. Below is the code snippet of the plugin: const gaugeNeedle ...

Eliminate using a confirmation popup

My attempts to delete an employee with a confirmation dialog are not successful. I have already implemented a splice method in my service code. The delete function was functioning correctly before adding the confirmation feature, but now that I have upgrad ...

Provide the remaining arguments in a specific callback function in TypeScript while abiding by strict mode regulations

In my code, I have a function A that accepts another function as an argument. Within function A, I aim to run the given function with one specific parameter and the remaining parameters from the given function. Here's an example: function t(g: number, ...

Launching Nest.js application from Visual Studio Code

Currently experimenting with a new framework called which seems promising as it integrates TypeScript into Node, similar to Angular. I'm using the starter template from https://github.com/kamilmysliwiec/nest-typescript-starter. I can start it withou ...

Insert HTML elements into the variable that holds data retrieved from firestore

Recently, I received a Firestore response in the following format: https://i.sstatic.net/49dX9.png Within my TypeScript variable {{task.title}}, I have access to this data on my component. My goal is to incorporate a hyperlink specifically on the person& ...

How to simulate keyboard events when a dropdown list is opened in an Angular application

Requirement- A situation arises where upon opening the dropdown menu, pressing the delete key on the keyboard should reset the index to -1. Steps to reproduce the issue: 1. Click on the dropdown and select an option from the menu. 2. Click on the dropdow ...

Angular: detecting mobile status within template

Often in my templates, I find myself repeating this type of code: <custom-button [align]="isMobile() ? 'center' : 'left'"></custom-button> This also requires me to include a method in each component to determine w ...

Dynamically generating an Angular component and populating it with data

I am currently working with Angular 7/8 and I have some code that adds a new component dynamically. In the parent component, my .ts file includes the following: PARENT COMPONENT Within the .ts file: @ViewChild(InjectDirective) injectComp: InjectDirect ...

Deleting and inserting an element in the Document Object Model

I am currently working on developing a framework and need to create a directive called cp-if. Unlike the existing cp-show directive, where I can simply change the visibility of an element to 'none' and then make it visible again, with the cp-if d ...

What is the most efficient way to find the sum of duplicates in an array based on two different properties and then return the

var data = [ { "amount": 270, "xlabel": "25-31/10", "datestatus": "past", "color": "#E74C3C", "y": 270, "date": "2020-10-31T00:00:00Z", "entityId": 1, "entityName": "Lenovo HK", "bankName": "BNP Paribas Bank", "b ...

Encountering errors in Visual Studio when trying to work with node_modules directories that have a tsconfig

In my current project, there is a tsconfig.json file located in the root directory. Strangely, Visual Studio keeps throwing errors related to other instances of tsconfig.json found in different packages, as shown below: https://i.sstatic.net/T7Co2.png Ev ...

Using Typescript: Defining a function parameter that can be either of two interfaces

While browsing through this specific question, I noticed that it was somewhat related to my current issue, although there were notable differences. In my scenario, I have a function named parseScanResults which accepts an object as its argument. This obje ...

Having conflicting useEffects?

I often encounter this problem. When I chain useEffects to trigger after state changes, some of the useEffects in the chain have overlapping dependencies that cause them both to be triggered simultaneously instead of sequentially following a state change. ...

Strategies for avoiding unused style tags in React components

Expanding beyond React, I'm unsure if React itself is the culprit of this issue. In a React environment with TypeScript, I utilize CSS imports in component files to have specific stylesheets for each component. I assumed these styles would only be ad ...

Conceal a division based on its numerical position

I'm having trouble figuring out how to hide multiple divs based on an index that I receive. The code snippet below hides only the first div with the id "medicalCard", but there could be more than one div with this id. document.getElementById("medical ...

Universal Angular along with Window's Innerwidth

Utilizing Window.Innerwidth in my Angular Project has been successful, however, I encountered an issue when attempting to implement it into Angular Universal. The content within window.innerwidth does not appear in the view source. Here is a snippet of my ...

The error message SCRIPT1003 is indicating that a colon was expected

There are many questions with this title, but I have a unique one for you. An error message "SCRIPT1003: Expected ':' (1,78)" pops up when I launch my website. I am using webpack and typescript in my project. Below is my tsconfig: { "co ...

Tips for specifying types in protractor.conf.js while utilizing the @ts-check feature

Within my Angular CLI v7.3.6 project, there is a protractor.conf.js file that I'm looking to enhance with @ts-check in VSCode. When using @ts-check, I aim to execute the browser.getCapabilities() function in the onPrepare() callback but encountered an ...