Save a collection of interfaces/types in TypeScript

Exploring a new approach has presented me with a minor challenge:

This is what I have=>

export interface SynchGroupSubject {
  type: SynchGroupEvent;
  target: any;
}

export enum SynchGroupEvent {
  ADD_MAP,
  REMOVE_MAP
}

Within a service, the following code exists

  public createNewSynchGroup(id?: string): Observable<SynchGroupSubject> {
    const subject = new Subject<SynchGroupSubject>();
    this.synchronizedGroupSubject.set(id, subject);
    return subject.asObservable();
  }

By subscribing to the update event, I can follow along =>

this.synchObs = this.mapSynchService.createNewSynchGroup(this.synchID);
this.synchObs.subscribe(event => this.synchEventHandler(event));
synchEventHandler(event: SynchGroupSubject) {
 switch (event.type) {
  case SynchGroupEvent.ADD_MAP:
       //event.target is of type any
    break;
  default:
  break;

 }
}

The issue lies in the fact that I would like to specify the type of the target so that during the switch statement, I am aware of what I am dealing with. Currently, the target is of type any, but I desire to bind a type relevant to the event type.

One way to achieve this goal would be to create multiple types, as shown below :

export interface ADD_MAP_TYPE {
  type: SynchGroupEvent;
  target: MyTypedTarger;
}

and then do

synchEventHandler(event: SynchGroupSubject) {
 switch (event.type) {
  case SynchGroupEvent.ADD_MAP:
       event.target = event.target as ADD_MAP_TYPE;
    break;
  default:
  break;

 }

However, I prefer something like SynchGroupEventType.ADD_MAP.

I considered using namespaces, but it seems unnecessary.

In addition, if I want to dispatch an event

  public dispatchSynchedEvent(id: string, type: SynchGroupEvent, value: any) 
  {
     myElement.next({
       type: type,
       target: value,
     })
  }

Even here, my value is of type any, and I wish to be able to do the following

  public dispatchSynchedEvent(id: string, type: SynchGroupEvent, value: any) 
  {
     myElement.next({
       type: type,
       target: value as , // get the type of the value thx to the SynchGroupEvent type
     })
  }

Answer №1

If you want a cleaner solution for handling different types of target in your SynchGroupSubject, consider utilizing a discriminated union. Instead of relying on type assertions to specify the target type when checking the type property, discriminated unions offer an elegant way to handle this mapping automatically. Here's an example to showcase this concept:

export enum SynchGroupEvent {
    ADD_MAP,
    REMOVE_MAP
}

// Sample target interfaces    
interface MyAddTarget { a: string };
interface MyRemoveTarget { r: number };

// Using a discriminated union
type SynchGroupSubject = {
    type: SynchGroupEvent.ADD_MAP,
    target: MyAddTarget
} | {
    type: SynchGroupEvent.REMOVE_MAP,
    target: MyRemoveTarget
}

function synchEventHandler(event: SynchGroupSubject) {
    switch (event.type) {
        case SynchGroupEvent.ADD_MAP:
            event.target; // Type inference ensures it's MyAddTarget
            event.target.a; // No issues
            event.target.r; // Compilation error
            break;
        default:
            event.target; // Compiler recognizes it as MyRemoveTarget
            event.target.a; // Compilation error
            event.target.r; // Works fine
            break;
    }
}

By structuring your code using a discriminated union, you can achieve strong typing and avoid manual type tracking. The Playground link provides an interactive demo to further explore this setup.

Hopefully, this explanation clarifies the benefits of discriminated unions for your scenario. Best of luck with your TypeScript development!

Answer №2

@jcalz's answer is the recommended approach and should be accepted as the solution. It is advised to read through his response first to grasp the concept of a discriminated union.

In addition to jcalz's method, I would like to propose an alternative technique or "trick" that may prove useful under certain circumstances.

If your SynchGroupSubject adheres to a consistent structure such as { type, target }, you can consider defining your target type in the following manner for better readability:

enum SynchGroupEvent {
    ADD_MAP,
    REMOVE_MAP
}

type SynchGroupEventTarget = {
    [SynchGroupEvent.ADD_MAP]: { a: string };
    [SynchGroupEvent.REMOVE_MAP]: { r: number }
}

Here's the trick - instead of manually creating a union type, you can utilize mapped types to derive the discriminated union SynchGroupSubject from the two aforementioned types.

type SynchGroupSubject = {
    [K in SynchGroupEvent]: {
        type: K;
        target: SynchGroupEventTarget[K];
    }
}[SynchGroupEvent]

An added benefit of this approach is code reusability. If a similar pattern emerges frequently, you can abstract it into a utility type.

type BuildSubject<Enum extends keyof TargetMap, TargetMap> = {
    [K in Enum]: {
        type: K;
        target: TargetMap[K]
    }
}[Enum]

// implementation
type SynchGroupSubject = BuildSubject<SynchGroupEvent, SynchGroupEventTarget>
type OtherSubject = BuildSubject<OtherEvent, OtherEventTarget>

It's important to note that the assumption of fixed shapes in these Subject types is crucial for this method to function correctly.

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

The module for the class could not be identified during the ng build process when using the --

Encountering an error when running: ng build --prod However, ng build works without any issues. Despite searching for solutions on Stack Overflow, none of them resolved the problem. Error: ng build --prod Cannot determine the module for class X! ...

What is the solution for addressing the deprecation warning "The 'importsNotUsedAsValues' flag will no longer work in TypeScript 5.5"?

Is anyone familiar with how to resolve this tsconfig error? The flag 'importsNotUsedAsValues' is outdated and will no longer work in TypeScript 5.5. To address this error, use 'ignoreDeprecations: "5.0"' or switch to using & ...

Ensuring that the field is empty is acceptable as long as the validators are configured to enforce

I have successfully created a form using control forms. idAnnuaire: new FormControl('',[Validators.minLength(6),Validators.maxLength(6)]), However, I am facing an issue where when the field is left empty, {{form.controls.idAnnuaire.valid }} is ...

How can I efficiently map an array based on multiple other arrays in JavaScript/TypeScript using ES6(7) without nested loops?

I am dealing with 2 arrays: const history = [ { type: 'change', old: 1, new: 2 }, { type: 'change', old: 3, new: 4 }, ]; const contents = [ { id: 1, info: 'infor1' }, { id: 2, info: 'infor2' }, { id: ...

Buffer.from in Node.js exposes program context leakage

Have you encountered a bug where the Buffer.from() function reads outside of variable bounds when used with strings? I experienced some unusual behavior on my backend, where concatenating 2 buffers resulted in reading contents of variables and beyond, inc ...

Developing a TypeScript NodeJS module

I've been working on creating a Node module using TypeScript, and here is my progress so far: MysqlMapper.ts export class MysqlMapper{ private _config: Mysql.IConnectionConfig; private openConnection(): Mysql.IConnection{ ... } ...

Angular's table data display feature is unfortunately lacking

Below is a simple HTML code snippet: <div class="dialogs"> <div id="wrapper" > <p>{{createTestingConstant()}}</p> <ng-container *ngFor="let one of contacts"> <p>{{one ...

React Functional Component not working properly following package update

After a 4-month hiatus from programming, I decided to update this project using npm but encountered issues with all my stateless functions. interface INotFoundPageContainerProps { history: any; } class NotFoundPag ...

Is there a user-friendly interface in Typescript for basic dictionaries?

I'm not inquiring about the implementation of a dictionary in Typescript; rather, I'm curious: "Does Typescript provide predefined interfaces for common dictionary scenarios?" For instance: In my project, I require a dictionary with elements of ...

Take a look at the browser's view

Are there any methods to monitor changes in the browser window using an Observable, such as rxJS or a similar tool? I am interested in triggering an action whenever the browser window is resized. ...

Get your hands on the latest version of Excel for Angular

214/5000 I am currently facing an issue in Angular where I am attempting to generate an excel file. Within the file, there is a "Day" column that is meant to display numbers 1 through 31. However, when attempting this, only the last number (31) is being pr ...

You cannot assign type void to type any

I'm currently working on a component that involves some code: export class AddNewCardComponent { public concept = []; constructor( private _router: Router, private _empDiscService: empDiscService) { } ngOnIni ...

Typescript is being lenient with incorrect use of generics, contrary to my expectations of error being thrown

Encountered a puzzling Typescript behavior that has left me confused. Take a look at the following code snippet: interface ComponentProps<T> { oldObject: T } function Component<T>({ oldObject }: ComponentProps<T>) { const newObject = ...

Encountering the following error message: "Received error: `../node_modules/electron/index.js:1:0 Module not found: Can't resolve 'fs'` while integrating next.js with electron template."

I am utilizing the electron template with next.js, and I am trying to import ipcRenderer in my pages/index.tsx file. Below is the crucial code snippet: ... import { ipcRenderer } from 'electron'; function Home() { useEffect(() => { ip ...

Serverless-offline is unable to identify the GraphQL handler as a valid function

While attempting to transition my serverless nodejs graphql api to utilize typescript, I encountered an error in which serverless indicated that the graphql handler is not recognized as a function. The following error message was generated: Error: Server ...

The issue with ag-grid not displaying data when the column configurations are changed dynamically

I have been working with ag grid to display data in my component. I am fetching data through an API call and dynamically extracting the column names from the response to pass this information to the child component for display. However, the data is not sho ...

Developing a Location instance with TypeScript

Struggling with redirecting my app outside of Angular to the logout page. I've tried using $window.location.href, but it doesn't work in Firefox. Someone recommended using $window.location directly, but I'm writing in TypeScript so I need t ...

In Vue using Typescript, how would I go about defining a local data property that utilizes a prop as its initial value?

When passing a prop to a child component in Vue, the documentation states: The parent component updates will refresh all props in the child component with the latest value. Avoid mutating a prop inside a child component as Vue will warn you in the consol ...

Alter text within a string situated between two distinct characters

I have the following sentence with embedded links that I want to format: text = "Lorem ipsum dolor sit amet, [Link 1|www.example1.com] sadipscing elitr, sed diam nonumy [Link 2|www.example2.com] tempor invidunt ut labore et [Link 3|www.example3.com] m ...

"Error: The term 'Readable' is not

When I input this code: const { Readable } = require('stream'); Readable ends up being undefined. However, when I try this code instead: import { Readable } from 'stream'; Readable becomes an empty object with no properties. It&apos ...