Is it possible to broaden Tagged/Discriminated Union in TypeScript within a separate module?

In my system, I have established a method for transferring JSON messages through a socket connection. The communication involves Tagged Unions to categorize different message types:

export type ErrorMessage = { kind: 'error', errorMessage: ErrorData };
export type UserJoined = { kind: 'user-joined', user: UserData };
// etc
export type Message = ErrorMessage | UserJoined | /*etc*/;

While this setup is effective in the foundational code, I am now looking to enhance it within an additional module by introducing a new message type:

export type UserAction = { kind: 'user-action', action: Action }

The challenge arises when attempting to extend the existing "Message" union to include the new UserAction. One solution could be creating a separate extended message type:

export type ExtendedMessage = Message | UserAction;

However, this approach feels cumbersome and restrictive. It prevents passing UserAction as part of methods that expect a Message, even though logically it should function correctly. Moreover, future developers seeking to expand both modules will need to create yet another type like

export type ExtendedMessageAgain = ExtendedMessage | MyNewMessage
.

This situation prompted me to explore if there is a way to augment tagged unions across multiple modules without establishing a new type under a changed name or implementing a different design altogether.

I have researched various techniques used to extend interfaces with supplemental properties by introducing new .d.ts files, such as how passport extends Express JS's Request object for authentication functionalities. However, I have not come across a similar pattern for extending Tagged Unions in my searches, leading me to question the effectiveness of my current design.

My reluctance towards using classes stems from the fact that type information gets lost during data transmission, necessitating the existence of the kind property. Despite the constraints, I value the provided paradigm:

declare var sendMessage = (message: Message) => void;
sendMessage( { kind: 'error', errorMessage: { /* */ } }); // ok
sendMessage( { kind: 'random', parameter: { /* */ } }); // error, no kind 'random'
sendMessage( { kind: 'error', message: { /* */ } }); // error, no property 'message' on 'error'

Considering these factors, the only conceivable resolution I see involves transforming Message into an interface foundation, illustrated here:

export interface Message { kind: string }
export interface ErrorMessage extends Message { errorMessage: ErrorData }

declare var sendMessage = (message: Message) => void;

sendMessage( { kind: 'error', errorMessage: { /* */ } }); // ok
sendMessage( { kind: 'random', parameter: { /* */ } }); // ok
sendMessage( { kind: 'error', message: { /* */ } }); // ok

However, employing this method sacrifices the stringent type protections previously enjoyed.

With these considerations in mind, I am left wondering if there exists a technique to effectively extend Tagged Unions across multiple modules while preserving the original type name, devoid of creating a new definition. Alternatively, I remain open to exploring alternative design approaches that might offer a more elegant solution.

To better understand the context behind this discussion, you can refer to the following code: https://github.com/RonPenton/NotaMUD/blob/master/src/server/messages/index.ts

Ultimately, my goal is to streamline this structure by segregating all message components into distinct modules, preventing the current consolidated file from becoming unwieldy over time.

Answer №1

To establish the union type of Message, you can follow these steps:

export interface MessageTypes {}
export type Message = MessageTypes[keyof MessageTypes]

Whenever you create a new message type, implement this approach:

export type UserAction = { kind: 'user-action', action: Action }

declare module '../message' { // Location for defining MessageTypes
  interface MessageTypes {
    UserAction: UserAction
  }
}

This method allows the MessageTypes interface values to form a union type. By utilizing declaration merging, additional values can be appended to the interface, thereby automatically updating the union type.

For more details on declaration merging in TypeScript, refer to their documentation here: https://www.typescriptlang.org/docs/handbook/declaration-merging.html

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

What is the process of exporting a generator function in TypeScript?

Here is an example I have in JavaScript: export function* mySaga () { yield takeEvery('USER_FETCH_REQUESTED', fetchUser); } I'm wondering if it's possible to rewrite it as an arrow function in TypeScript, like the snippet below: ...

Tips for configuring the Index column within an Angular Mat-table (when the dataIndex displays 'NaN')

My Mat-Table is working perfectly, but I am looking for a way to add an auto-increment index. Below is the HTML code: <div class="mat-elevation-z8"> <table mat-table [dataSource]="dataSource" matSort> <ng-container matColumnDef="no"> ...

Guide to setting up react-styleguidist, developing with Create React App, using Typescript, incorporating Material UI, and including

Struggling to configure react-styleguidist (RSG) with Create React App 3 (CRA), Typescript, Material UI, and styled-components. Currently encountering an error: ./node_modules/react-styleguidist/lib/client/rsg-components/ReactExample/ReactExample.js Modul ...

Attempting to compile TypeScript by referencing ng2-bootstrap using Gulp within Visual Studio

I've been struggling with this issue for a few days now, and I'm really hoping someone can help me out. Currently, I am experimenting with Angular2 in an aspnet core project. The setup involves using a gulpfile.js to build .ts files and transfer ...

Issue when transferring properties of a component to a component constructed Using LoadingButton MUI

Check out my new component created with the LoadingButton MUI: view image description here I'm having issues passing props to my component: view image description here Dealing with a TypeScript problem here: view image description here I can resolv ...

Trouble with importing React JSX from a separate file when working with Typescript

This problem bears some resemblance to How to import React JSX correctly from a separate file in Typescript 1.6. Everything seems to be working smoothly when all the code is contained within a single file. However, as soon as I move the component to anoth ...

Identify the nature of the output received after dispatching

I'm developing a functional component within the realm of Redux, and I have configured it to return specific values for a particular action. The new value being returned is derived from a Promise, meaning that if the type is designated as "Ival," the ...

Is "as" truly necessary in this context?

After following a tutorial, I created a class and noticed that the interface was declared with an as name. I'm wondering if this is necessary. What is the purpose of reassigning it when it was already assigned? My TypeScript code: import { Component ...

Steps for calculating the average of several columns within a table using Angular 10

Currently, I have a function that successfully calculates the sum of JSON data in all columns on my tables. However, my attempt to get the average of each column is resulting in NaN or infinity. What could be the issue here? Here is my current implementat ...

Determine the type of a nested class within TypeScript

Utilizing nested classes in TypeScript is achieved through the following code snippet: class Parent { private secret = 'this is secret' static Child = class { public readSecret(parent: Parent) { return parent.secret } } } ...

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 ...

Heroku is showing an error: "SyntaxError: Import statement cannot be used outside a module."

An unusual bug occurred when attempting to deploy my TypeScript-written node.js backend to Heroku. The code functioned flawlessly in my local environment, as well as on all of my teammates' machines, but it encountered issues on Heroku. The error from ...

Block-level declarations are commonly used in TypeScript and Asp.net MVC 5

In my asp.net mvc5 project, I decided to incorporate TypeScript. I created an app.ts file and installed the nuget-package jquery.TypeScript.DefinitelyTyped. Here is a snippet of the app.ts code: /// <reference path="typings/jquery/jquery.d.ts"/> cl ...

Best practices for organizing an array of objects in JavaScript

I have an array of objects with nested arrays inside, and I need to restructure it according to my API requirements. [{ containerId: 'c12', containerNumber: '4321dkjkfdj', goods: [{ w ...

What is the best way to update the state of a response from an API call for a detailed object using React context, so that there is no need to retrigger the API call

I'm currently developing a react native typescript project. How can I assign the data received from an API call to my context object? I want to avoid making the API call every time the component is loaded. Instead, I want to check if the context alr ...

Leverage the event handler within a React Component when working with TSX tags

Is there a way to expose an event handler that is defined within a React Component in an HTML-like tag? For example: <MyComp param1="abc" param2="def" onDoSomething={this.someMethod} /> I am trying to define the onDoSomething event, but currently I ...

What could be causing the TypeScript exhaustive switch check to malfunction?

How come the default case in the switch statement below does not result in an exhaustive check where 'item' is correctly identified as of type 'never'? enum Type { First, Second } interface ObjectWithType { type: Type; } c ...

How can I receive live notifications for a document as soon as it is created?

My Angular app is connected to Cloud Firestore, and I've created a function in a service to retrieve a user's rating from the 'ratings' collection. Each rating is stored in this collection with the document ID being a combination of the ...

Ensuring a Generic Type in Typescript is a Subset of JSON: Best Practices

I am interested in achieving the following: interface IJSON { [key: string]: string | number | boolean | IJSON | string[] | number[] | boolean[] | IJSON[]; } function iAcceptOnlyJSON<T subsetof IJSON>(json: T): T { return json; ...

Do not include the "dist" folder when exporting sub-modules in TypeScript

I've developed a basic module called modA: modA/ - package.json - dist/ - index.js - db.js - stuff.js My goal is to use the submodules "db" and "stuff" like this: import * as db from modA/db -- how can I achieve this? My package.json has main: ...