A method in an abstract TypeScript class that accepts arguments of various union types

Class Structure

enum VehicleType {
  car = 'CAR',
  ship = 'SHIP',
}

interface BaseMoveDto {
  type: VehicleType;
}

interface CarMoveDto extends BaseMoveDto {
  type: VehicleType.car;
}

interface ShipMoveDto extends BaseMoveDto {
  type: VehicleType.ship;
}

type VehicleMoveDto = CarMoveDto | ShipMoveDto;

abstract class Vehicle {
  abstract move(dto: VehicleMoveDto): VehicleMoveDto;
}

class Car extends Vehicle {
  move(dto: CarMoveDto) {
    return dto;
  }
}

class Ship extends Vehicle {
  move(dto: ShipMoveDto) {
    return dto;
  }
}

Implementation Overview

class VehicleService {
  getVehicle(type: VehicleType) {
    switch (type) {
      case VehicleType.car:
        return new Car();
      case VehicleType.ship:
        return new Ship();
      default:
        throw new Error('invalid vehicle type');
    }
  }

  calculateMovement(dto: VehicleMoveDto) {
    const vehicle = this.getVehicle(dto.type);
    return vehicle.move(dto);
  }
}

Encountered Issue

Argument of type 'VehicleMoveDto' is not assignable to parameter of type 'never'.
  The intersection 'ShipMoveDto & CarMoveDto' was reduced to 'never' because property 'type' has conflicting types in some constituents.
    Type 'CarMoveDto' is not assignable to type 'never'.

https://i.sstatic.net/jlx0J.png

The error message indicates that Typescript is intersecting move() arguments instead of using union, resulting in the dto argument becoming type never and causing an error. Is there anything I may have misunderstood about typescript? Are there any solutions available to achieve the desired outcome?

Alternative Approaches

I understand that I can implement it without encountering any issues.

class VehicleService {
  calculateMovement(dto: VehicleMoveDto) {
    switch (dto.type) {
      case VehicleType.car:
        return new Car().move(dto);
      case VehicleType.ship:
        return new Ship().move(dto);
      default:
        throw new Error('invalid vehicle type');
    }
  }
  otherMethod(type: VehicleType) {
    // another method utilizing a switch case
  }
}

However, with the above implementation, I am unable to reuse the switch case for other methods.

Answer №1

The issue here lies in the inability of the compiler to comprehend the relationship between the type of vehicle and the type of dto. It interprets vehicle as Car | Ship and dto as CarMoveDto | ShipMoveDto. These unrelated union types are not necessarily incorrect, but they lack sufficient information for the compiler to ensure the safety of vehicle.move(dto). The compiler may assume that vehicle is a Car, while dto is a ShipMoveDto, which conflicts with reality. I have raised an issue, microsoft/TypeScript#30581, requesting support for correlated union types, although it might be challenging to address this effectively.

To work around this limitation, you can either prioritize type-safety at the cost of redundancy or opt for convenience over strict typing.


In the redundant approach, you explicitly specify all possible scenarios, similar to the example you provided where you guide the compiler through every option. Your getVehicle() function lacks adequate type information since it always returns Car | Ship. Modifying it as follows:

getVehicle<T extends VehicleType>(type: T) {
  return {
    get [VehicleType.car]() {
      return new Car();
    },
    get [VehicleType.ship]() {
      return new Ship();
    }
  }[type]
}

will help the compiler understand that this.getVehicle(Vehicle.car) results in a Car instance rather than Car | Ship. By utilizing generics and object property lookup with getters, you can then write:

calculateMovementRedundant(dto: VehicleMoveDto) {
  return dto.type === VehicleType.car ?
    this.getVehicle(dto.type).move(dto) :
    this.getVehicle(dto.type).move(dto);
}

Although effective, this method is brittle and repetitive. I would recommend it only if strict type safety outweighs simplicity and conventionality.


As for the convenient approach, you maintain the same emitted JavaScript structure while employing type-unsafe practices like type assertions to alleviate the compiler's concerns regarding incompatible cross-correlated dto/vehicle types.

In your scenario, the abstract Vehicle class’s getVehicle() method already features a technically unsafe typing. The parameter type VehicleMoveDto encompasses too broad a range, as subclasses of Vehicle cannot reasonably accept any arbitrary VehicleMoveDto. You leverage TypeScript’s parameter bivariance, even when --strictFunctionTypes is enabled.

By annotating vehicle as a Vehicle instead of letting the compiler infer it as Car | Ship, you can now pass a CarMoveDto | ShipMoveDto as shown below:

calculateMovement(dto: VehicleMoveDto) {
  const vehicle: Vehicle = this.getVehicle(dto.type); // risky "widening"
  return vehicle.move(dto); // no error
}

This straightforward adjustment allows the code to compile successfully. However, bear in mind that maintaining type integrity falls on your shoulders since the compiler cannot provide assistance. If you take liberties like in the following example:

calculateMovementOops(dto: VehicleMoveDto) {
  const vehicle: Vehicle = Math.random() < 0.5 ? new Car() : new Ship(); // potential issue
  return vehicle.move(dto); // still no error
}

The compiler won't flag errors, but runtime issues could arise unexpectedly. Exercise caution when pursuing this route.

Playground link showcasing the code

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

Receiving a null value when accessing process.env[serviceBus]

Currently, I am focusing on the backend side of a project. In my environment, there are multiple service bus URLs that I need to access dynamically. This is how my environment setup looks like: SB1 = 'Endpoint=link1' SB2 = 'Endpoint=link2&a ...

Compose a message directed to a particular channel using TypeScript

Is there a way to send a greeting message to a "welcome" text channel whenever a new user joins the server (guild)? The issue I'm running into is that, when I locate the desired channel, it comes back as a GuildChannel. Since GuildChannel does not hav ...

Angular is putting the page on ice - all clicks are officially off limits

When making an HTTP request to the backend in my project, I need the ability to sort of "freeze" the screen until the request is complete. Specifically, I want to prevent users from being able to interact with certain elements on the page, such as a butt ...

Setting up Webpack to compile without reliance on external modules: A step-by-step guide

I am facing an issue with a third-party library that needs to be included in my TypeScript project. The library is added to the application through a CDN path in the HTML file, and it exports a window variable that is used in the code. Unfortunately, this ...

Angular // binding innerHTML data

I'm having trouble setting up a dynamic table where one of the cells needs to contain a progress bar. I attempted using innerHTML for this, but it's not working as expected. Any suggestions on how to approach this? Here is a snippet from my dash ...

Make sure that every component in create-react-app includes an import for react so that it can be properly

Currently, I am working on a TypeScript project based on create-react-app which serves as the foundation for a React component that I plan to release as a standalone package. However, when using this package externally, I need to ensure that import React ...

Is it possible to define an object literal type in typescript that permits unspecified properties?

I am looking to make sure that an object argument has all the necessary properties, while also allowing for additional properties. For instance: function verifyObject(input: { key: string }) : number { return input.key; } verifyObject({ key: 'va ...

Utilizing Typescript with Mongoose Schemas

Currently, I am attempting to connect my Model with a mongoose schema using Typescript. Within my IUser interface: export interface IUser{ _id: string; _email: string; } I also have a User class: export class User implements IUser{ _id: string; ...

Maintaining type information while iterating over an object with Typescript

I am faced with the challenge of wrapping functions within an object in order to use their return values, all without altering their signature or losing type information. // An object containing various functions const functions = { foo, bar, baz } // Exa ...

Contrasting Compositions with Generics

Let's consider a scenario where we have an abstract class A and three concrete classes that inherit from it: A1, A2, and A3. There is also another hierarchy tree with an abstract class B and three concrete classes B1, B2, and B3. Each concrete class A ...

Using TypeScript with axios and encountering an undefined property

Currently, I am encountering an issue while attempting to type the axios response. Here is a glimpse of how the response type appears: export interface GetBreedsResponse { data: { message: Breed[] } } Within my router file, I have implemented the ...

Mastering the art of utilizing GSI Index and FilterExpression in AWS DynamoDB querying

In my DynamoDB database table, I have the following structure. It consists of a primary key (ID) and a sort key (receivedTime). ID(primary key) receivedTime(sort key) Data ID1 1670739188 3 ID2 167072 ...

What is the process for creating an Angular library using "npm pack" within a Java/Spring Boot application?

In my angular project, we have 5 custom libraries tailored to our needs. Using the com.github.eirslett maven plugin in our spring boot application, we build these libraries through the pom.xml file and then copy them to the dist/ folder. However, we also ...

How to conditionally apply a directive to the same tag in Angular 4

I am implementing angular 4 and have a directive in my template for validation purposes. However, I would like to first check if a specific condition is true before applying the directive. Currently, my code looks like this: <div *ngIf="groupCheck; els ...

Concealed Dropdown Option

<div fxFlex="25" fxFlex.xs="100" class="px-8"> <div class="form-label">Reporting Status <span class="reqSgnColor">*</span> </div> <mat-form-field appearance=&quo ...

Exploring the possibilities of Ionic 2 with Web Audio API

I am encountering issues while using the Web Audio API with Ionic 2. Despite my efforts, I keep running into errors. It seems that the problem lies within the TypeScript compiler. I attempted to resolve it by adding "es2015.promise", but to no avail. The e ...

Exploring the customization options for Prime NG components is a great way to

Currently, I am working on a project that involves utilizing Prime NG components. Unfortunately, the p-steps component does not meet one of our requirements. I am looking to customize the Prime NG p-steps component to fit our needs. Is there a way to cre ...

Dealing with text overflow within a column and ensuring that the text always expands horizontally to align with the column width

I have implemented a file input feature that displays the selected file's name in a column. However, when the file name is too long, it expands vertically which causes readability issues. There is an option to force the text to expand horizontally, b ...

How to convert DateTime to UTC in TypeScript/JavaScript while preserving the original date and time

Consider the following example: var testDate = new Date("2021-05-17T00:00:00"); // this represents local date and time I am looking to convert this given Date into UTC format without altering the original date and time value. Essentially, 2021-0 ...

Show categories that consist solely of images

I created a photo gallery with different categories. My goal is to only show the categories that have photos in them. Within my three categories - "new", "old", and "try" - only new and old actually contain images. The issue I'm facing is that all t ...