Mapping enum types in Typescript with overlapping data can be achieved by following these steps

I am currently working on a project where I am creating a type that maps different enum types to expected data types. My goal is to accurately determine the correct type of the data through type inference.

enum Car {
    BMW,
    Toyota,
    Fiat
}

type CarWithData =
    | {
            type: Car.BMW;
            data: {
                doucheFactor: number;
            };
      }
    | {
            type: Car.Toyota;
            data: {
                dadFactor: number;
            };
      }
    | {
            type: Car.Fiat;
            data: {
                dadFactor: number;
            };
      };

function handleCar(car: CarWithData) {
    switch (car.type) {
        case Car.BMW:
            console.log(`BMW owner found with douche factor of: ${car.data.doucheFactor}`);
            console.log(`DadFactor: ${car.data.dadFactor}`); // TypeError as expected / intended;
            break;
        default:
            console.log(`Toyota / Fiat owner found with dad factor of: ${car.data.dadFactor}`);
    }
}

function getDadFactor(car: CarWithData): number {
    return car.data.dadFactor ?? -1; // Property "dadFactor" does not exist on type CarWithData
}

handleCar({ type: Car.BMW, data: { doucheFactor: 900 } });
getDadFactor({ type: Car.Fiat, data: { dadFactor: 10} });

In this scenario, my code successfully utilizes type inference in the switch statement within handleCar. However, an issue arises in the getDadFactor function where a TypeError is thrown due to "dadFactor" not being recognized under the type CarWithData. Can you suggest a more optimal solution than including all possible fields with type never in a union?

Answer №1

If we start by checking the car.type, it seems like the most straightforward solution.

function calculateDadRating(car: CarWithData): number {
  return car.type === Car.Toyota ? car.data.dadFactor : -1;
}

However, in cases where this approach doesn't quite fit due to multiple scenarios, you can implement a custom type guard to confirm that the specified car has the dadFactor:

const hasDadFactor = (
  arg: CarWithData,
): arg is CarWithData & { data: DadFactorData } => {
  return typeof (arg.data as DadFactorData).dadFactor === 'number';
};

function getDadRating2(car: CarWithData): number {
  return hasDadFactor(car) ? car.data.dadFactor : -1;
}

Alternatively, you can define a specific type for cars with the dadFactor attribute only.

type CarsWithDadRating<T extends CarWithData = CarWithData> = T extends T
  ? 'dadFactor' extends keyof T['data']
    ? T
    : never
  : never;
function calculateDadRating3(car: CarsWithDadRating): number {
  return car.data.dadFactor;
}

The unique aspect of the third option lies in the usage of T extends T, akin to iterating through a union type.

sandbox

For a more generic adaptation of the third method, an additional generic parameter specifying the type of data desired can be included:

type CarsWithSpecificData<D, T extends CarWithData = CarWithData> = T extends T
  ? T['data'] extends D
    ? T
    : never
  : never;


function calculateDadRating3(car: CarsWithSpecificData<{dadFactor: number}>): number {
  return car.data.dadFactor;
}

sandbox

Answer №2

Here's a potential resolution:

type ExtractData<T> = T extends { info: infer U } ? U : never;

type AllInfoTypes = ExtractData<CarWithInfo>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never;

type CarWithPartialInfo = {
    category: Car;
    info: Partial<UnionToIntersection<AllInfoTypes>>;
};

function handleCategory(car: CarWithInfo) {
    switch (car.category) {
        case Car.BMW:
            console.log(`BMW owner discovered with accessory factor of: ${car.info.accessoryFactor}`);
            break;
        default:
            console.log(`Toyota / Fiat owner spotted with speed factor of: ${car.info.speedFactor}`);
    }
}

function getSpeedFactor(car: CarWithData): number {
    return (car as CarWithPartialInfo).info.speedFactor ?? -1;
}

This approach introduces a fresh type where the information type is the partial union of all potential data types. Although not aesthetically pleasing, difficult to decipher, and necessitating casting, it does deliver the desired functionality.

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

Leverage ngFor to loop through a "highly intricate" data structure

In my project, I have stored data in a JSON file structured as follows: { "name": { "source1": ____, "source2": ____, "source3": ____ }, "xcoord": { "source1": ____, "source2": ____, "source3": _ ...

ngOnInit unable to properly listen to event stream

I've been trying to solve a persistent issue without success. The problem involves the interaction between three key elements: the HeaderComponent, TabChangingService, and TabsComponent. Within the HeaderComponent, there are three buttons, each with a ...

Is there a way to extract both a and b from the array?

I just started learning programming and I'm currently working on creating an API call to use in another function. However, I've hit a roadblock. I need to extract values for variables a and b separately from the response of this API call: import ...

The reducer with typing indicated that it is only able to process the type of "never."

I'm currently working on implementing a typed reducer for creating a slice. To start, I define the IFeatureState interface and initialState as follows: interface IFeatureState { content: string; open: boolean; } const initialState: IFeatureState ...

Developing various VueJS TypeScript projects utilizing a shared library

In the process of developing two VueJS applications using TypeScript, I have created one for public use and another as an admin tool exclusively for my own use. Both applications are being built and tested using vue-cli with a simple npm run serve command. ...

typescript unconventional syntax for object types

As I was going through the TypeScript handbook, I stumbled upon this example: interface Shape { color: string; } interface Square extends Shape { sideLength: number; } var square = <Square>{}; square.color = "blue"; square.sideLength = 10; ...

Combining property values based on a common property in an array of objects using JavaScript

I have a large array filled with various objects structured like: [ { "type": "bananas", "count": 15 }, { "type": "kiwis", "count": 20 }, { "type": "bananas", ...

Lerna and Create React App (CRA) problem: When using the command "lerna run --parallel start", the devServer does not initiate

I am currently working on managing 2 projects within lerna packages. One project is a package, and the other is a CRA website. When I run "yarn start" for each package individually, I can see the build folder and the website being loaded on the development ...

Is it possible to update a reference to an Observable within an Angular Template?

Is it possible to update the source of an Observable referenced in an Angular template? For instance, let's say we have this snippet in the template. {{ ( progress$ | async ) | date:'mm:ss'}} And we wish to modify the Observable that $pr ...

Changing JSON names to display on a webpage

I am looking to modify the name displayed in a json file for presentation on a page using ion-select. mycodehtml ion-select [(ngModel)]="refine" (ionChange)="optionsFn(item, i);" > <ion-option [value]="item" *ngFor="let item of totalfilter ...

Deciphering the inner workings of React's DOM updates and mastering its application

I have a project where I need to implement functionality with 3 buttons, but only 2 are relevant to my current issue. One button is meant to add elements to the user's website view, while the other should remove elements. Although I am new to React, ...

The property y is not found on type x during property deconstruction

After creating a straightforward projectname.tsx file to contain my interfaces/types, I encountered an issue: export interface Movie { id: number; title: string; posterPath: string; } In another component, I aimed to utilize the Movie interface to s ...

What is the correct way to convert a non-observable into an observable?

Can I convert a non-observable into an observable to receive direct image updates without having to refresh the page, but encountering this error: Type 'EntityImage[]' is missing the following properties from type 'Observable<EntityImage ...

How to retrieve TypeScript object within a Bootstrap modal in Angular

Unable to make my modal access a JavaScript object in the controller to dynamically populate fields. Progress Made: Created a component displaying a list of "person" objects. Implemented a functionality to open a modal upon clicking a row in the list. ...

Display embedded ng-template in Angular 6

I've got a template set up like this <ng-template #parent> <ng-template #child1> child 1 </ng-template> <ng-template #child2> child 2 </ng-template> </ng-template> Anyone know how t ...

What could be the reason why the getParentWhileKind method in ts-morph is not returning the anticipated parent of the

Utilizing ts-morph for code analysis, I am attempting to retrieve the parent CallExpression from a specific Identifier location. Despite using .getParentWhileKind(SyntaxKind.CallExpression), the function is returning a null value. Why is this happening? I ...

Error: MatDialogRef provider is missing in NullInjector

I am facing an issue with injecting MatDialogRef as per the documentation instructions: https://material.angular.io/components/dialog/overview When attempting to inject it, I encounter the following error: ERROR Error: StaticInjectorError[MatDialogRef]: ...

Adding a new property to the Express request object type: what you need to know

Recently, I developed a custom middleware that executes specific logic tasks. It operates by transforming the keys to values and vice versa within the req.body. Both the keys and values are strings, with built-in validation measures in place for safety. T ...

Linking ngModel to a Dynamic List of Checkboxes in Angular 2 Using Typescript

Uncertainty surrounds the correct method for binding and updating a model when dealing with dynamically generated checkboxes in an ASP.NET Core project with Angular 2. The struggle extends to even basic checkbox elements, as observed through personal exper ...

Angular 8 shows an error message stating "Unknown Error" when making a REST API request

My goal is to retrieve data from the server using a REST API service. The code snippet below is from my service.ts file: getCategories(): Observable<Category> { const httpOptions = { headers: new HttpHeaders({ 'Content-Type&a ...