Generics in Classes: Unintelligible Error

For a demonstration, you can check out the implementation in this codesanbox. The class "Operation.ts" contains all the details.

The purpose of the "Operation" class is to manage operations performed on objects such as rectangles. Each operation type ("move", "rotate", etc.) has its own specific operation data structure (move: [number, number], rotate: number, etc.). To accommodate these various types, I used generics within the class definition. The _type property represents the type T and the _operation property refers to OperationMap[T] to correctly identify the operation for each operation type.

After executing an operation using the Operation class, I aim to store both the original operationData and its reverse counterpart for future reference. However, I encountered two errors which I am unable to comprehend fully. Despite checking the _type variable to determine the type T, Typescript seems to struggle with recognizing the operationData and reversedOperationData types.

It appears there might be a gap in my understanding of how Typescript handles types in this context. I have scoured resources but haven't found a satisfactory explanation yet. Any assistance or insights would be greatly appreciated.

type operationTypes = "move" | "rotate" | "rename";

type operationsMap = {
  move: [number, number];
  rotate: number;
  rename: string;
};

type operationsDataMap = {
  move: [number, number];
  rotate: number;
  rename: [string, string]; // before, after
};

export class Operation<T extends operationTypes> {
  _type: T;
  _operation: operationsMap[T];
  _operationData: operationsDataMap[T] | null = null;
  _ReversedOperationData: operationsDataMap[T] | null = null;

  constructor(type: T, operation: operationsMap[T]) {
    this._type = type;
    this._operation = operation;
  }

  set operationData(operationData: operationsDataMap[T]) {
    this._operationData = operationData;
    if (this._type === "move") {
      this._ReversedOperationData = [-operationData[0], -operationData[1]]; // Issue lies here
    }
    if (this._type === "rotate") {
      // Handle reverse rotation
    }
    if (this._type === "move") {
      // Handle reverse renaming
    }
  }
}

Answer №1

The issue arises from the lack of support in TypeScript for using control flow analysis to influence generic type parameters. When checking if (this._type === "move"), the type of this._type gets narrowed, but K (renamed from T) remains unchanged. Consequently, the compiler is unaware that OperationsDataMap[K] will specifically be of type [number, number], causing a compilation error.

While there is a request for this feature at microsoft/TypeScript#33014, it is not yet integrated into the language. The interaction between generic type parameters and if/else or switch/case control flows is not seamless.


To make use of generics and align with your logic, you need to structure your code so that only one case works for all scenarios. TypeScript can recognize when you use a generic key to access an object. By defining an object's type as a mapped type consisting of functions, you can refactor if/else blocks as function bodies. This approach is detailed in microsoft/TypeScript#47109. Here's an example:

set operationData(operationData: OperationsDataMap[K]) {
  this._operationData = operationData;
  const rev: { [K in OperationTypes]: (x: OperationsDataMap[K]) => OperationsDataMap[K] } = {
    move: (x) => [-x[0], -x[1]], 
    rotate: x => -x,
    rename: x => [x[1], x[0]]
  }
  this._ReversedOperationData = rev[this._type](this._operationData);
}

In this setup, rev is explicitly annotated as a mapped type over OperationTypes, serving as the constraint for K. Each property represents a function mapping OperationsDataMap[K] to itself, validating the function body by the compiler. By calling

rev[this._type](this._operationData)
, the correct output is obtained, satisfying the assignment to this._ReversedOperationData.

Congratulations!


This refactoring technique may not always be straightforward or feasible. It relies on switching based on a key-like value for successful indexing into an object. If such conditions are not met, employing full-fledged solutions like those outlined in microsoft/TypeScript#47109 might be required, albeit complex for some individuals.

In situations where refactoring is challenging, resorting to type assertions as a temporary fix could be considered:

if (this._type === "move") {
  const o = operationData as [number, number];
  this._ReversedOperationData = [-o[0], -o[1]] as OperationsDataMap[K]
}

By utilizing type assertions, you essentially disable type checking temporarily and assume the responsibility of ensuring type safety instead of the compiler. This pragmatic approach can be chosen over compiler-verified type safety if speed is prioritized.

Playground link to code

Answer №2

According to your code, TypeScript cannot directly perform this type of operation using if/else or switch statements. However, it can be achieved using the as keyword which will also help resolve any errors.

type operationTypes = "move" | "rotate" | "rename";

type operationsMap = {
  move: [number, number];
  rotate: number;
  rename: string;
};

type operationsDataMap = {
  move: [number, number];
  rotate: number;
  rename: [string, string]; // before, after
};

export class Operation<T extends operationTypes> {
  _type: T;
  _operation: operationsMap[T];
  _operationData: operationsDataMap[T] | null = null;
  _ReversedOperationData: operationsDataMap[T] | null = null;

  constructor(type: T, operation: operationsMap[T]) {
    this._type = type;
    this._operation = operation;
  }

  set operationData(operationData: operationsDataMap[T]) {
    this._operationData = operationData;
    if (this._type === "move") {
        const Data = this._operationData as operationsDataMap['move']
        this._ReversedOperationData = [Data[0], Data[1]] as operationsDataMap[T];
    }
    if (this._type === "rotate") {
      // reversed rotation
      const Data = this._operationData as operationsDataMap['rotate']
        this._ReversedOperationData = Data as operationsDataMap[T];
    }
    if (this._type === "rename") {
      // reversed renaming
      const Data = this._operationData as operationsDataMap['rename']
      this._ReversedOperationData = Data as operationsDataMap[T];
    }
  }
}

const operationMethod = new Operation('rotate' , 1);

operationMethod.operationData

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

How to locate the position of an element within a multi-dimensional array using TypeScript

My data structure is an array that looks like this: const myArray: number[][] = [[1,2,3],[4,5,6]] I am trying to find the index of a specific element within this multidimensional array. Typically with a 1D array, I would use [1,2,3].indexOf(1) which would ...

TypeScript/Javascript - Error: The specified function is not callable

After recently delving into TypeScript, I found myself encountering an error in my code for a wheel mini-game on my app. The specific error being displayed in the console is: this.easeOut is not a function The relevant portion of the code causing the iss ...

Oops! There seems to be a hiccup: Unable to locate the control with the specified path: 'emails -> 0 -> email'

I am attempting to design a form incorporating a structure like this: formGroup formControl formControl formArray formGroup formControl formControl However, upon clicking the button to add reactive fields and submitting the form ...

Condition not applying in the Modal

I implemented *ngif on a button to show/hide it based on a condition, but it's not working as expected. The button should appear when an item is selected from ng-select. Here is the button code: <button *ngIf="switch" (click)="productSaveInCart() ...

Angular 4's OrderBy Directive for Sorting Results

I've been working on implementing a sorting pipe based on the code provided in this resource: The issue I'm facing revolves around handling undefined values within my data. The sorting pipe functions correctly when there are no undefined values ...

Guide to implementing lazy loading and sorting in p-Table with Angular2

I recently implemented lazy loading in my application and now I am having trouble with sorting items. When lazy loading is disabled, the sorting feature works perfectly fine. However, I need help to make both lazy loading and sorting work simultaneously. C ...

picker elementClass()

I encountered an issue while using the datepicker from Material UI. The datepicker has a method called dateClass: dateClass() { return (date: Date): MatCalendarCellCssClasses => { const unvailableArray = this.shareDate.unavailableDates; ...

Creating a feature that automatically determines the data type of a value using the provided key

My object type has keys that map to different types: type Value = { str: string; num: number; }; I am working on creating a universal format function: const format = <K extends keyof Value>(key: K, value: Value[K]): string => { if (key === ...

Discovering the file system with window.resolveLocalFileSystemURL in Typescript for Ionic 3

After reviewing the documentation found on this link for the File plugin, I came across a paragraph that discusses how to add data to a log file. See the example code below: window.resolveLocalFileSystemURL(cordova.file.dataDirectory, function (dirEntry) ...

Ways to set the className prop for all components automatically without having to specify it repeatedly

One challenge I face is dealing with code duplication whenever I create a new component. Is there a way to pass the className property between components without having to explicitly define it every time a new component is created? For example, when I cr ...

What is the reason behind Typescript raising an error when attempting to compare two boolean variables with different values (true and false)?

In the screenshot below, you can see that we are encountering a peculiar error when attempting to compare a boolean variable with true. This condition will always return 'false' since the types 'false' and 'true' have no over ...

In TypeScript, a mapped type is not allowed to define properties or methods

My challenge involves defining an interface with keys that match a specific enum key type. However, when I attempt to declare this type, I encounter the following error message: A mapped type may not declare properties or methods. Below is the code snip ...

Changing the generic type's constraint type in TypeScript to have more flexibility

I have developed a utility type named DataType that takes in a parameter T restricted to the type keyof MyObject. When the key exists in MyObject, DataType will return the property type from MyObject; otherwise, it will simply return T. interface MyObject ...

Injection of environmental variables into app services

Through the use of Nx, I have created multiple apps that each have their own environment with different API URLs. The Nx Workspace library includes shared services that are utilized among all apps, however, it is necessary to pass the environment-api-url w ...

Using Typescript to create an asynchronous function without explicitly declaring a Promise

When you examine TypeScript's async function, you may notice the redundancy with "async" and "Promise<type>". public async test(): Promise<string> { return "Test"; } Is there a way to configure TypeScript to handle async types ...

Why does Angular throw a length-related error, while I am able to retrieve the length using console log if needed?

It appears that Angular is not receiving the correct data type it expects, yet the lack of errors in the terminal is puzzling. However, the console output states: https://i.stack.imgur.com/1xPsg.jpg If the length property can be detected (highlighted in ...

Exploring an array of objects to find a specific string similar to the one being

I recently developed a TypeScript code snippet that searches for objects in a list by their name and surname, not strictly equal: list = list.filter( x => (x.surname + ' ' + x.name) .trim() .toLowerCase() .sear ...

Sending enums as arguments to a function

Is there a way to create a function that can work with any enum and function that accepts it as an argument? Consider the following scenario: enum Enum1 { VALUE1 = "value1" } enum Enum2 { VALUE2 = "value2" } const func1 = (e: Enum1) => e; const f ...

Resolving type error issues related to using refs in a React hook

I have implemented a custom hook called useFadeIn import { useRef, useEffect } from 'react'; export const useFadeIn = (delay = 0) => { const elementRef = useRef<HTMLElement>(null); useEffect(() => { if (!elementRef.current) ...

Changing a d3 event from JavaScript to Typescript in an Angular2 environment

I am a beginner in Typescript and Angular 2. My goal is to create an Angular2 component that incorporates a d3js tool click here. However, I am facing challenges when it comes to converting it to Typescript. For instance, I am unsure if this code rewrite ...