`Understanding the outcome type by assessing the (potential) attributes with TypeScript`

Here is a detailed example of a problem with some code:

type Mapper<T, R> = (data: T) => R;

interface Config<T, R> {
  readonly mapper?: Mapper<T, R>;
}

function transform<T, R>(config: Config<T, R>, data: T) {
  return config.mapper?.(data) ?? data;
}


const mapper: Mapper<number, string> = (data: number) => `${data};`
const result1 = transform({}, 1);
const result2 = transform({ mapper }, 1);

The question is how to properly type the result of the transform function. Please note that the function transform is a simplified example.


Now, let's consider the following code:

export type PendingState = {
  type: "PENDING";
};

export type SuccessState<T> = {
  type: "SUCCESS";
  data: T;
};

export type ErrorState = {
  type: "ERROR";
  error: unknown;
};

export type SuspenseState<T> = PendingState | SuccessState<T> | ErrorState;

export type PendingStateMapper<R> = () => R;
export type SuccessStateMapper<T, R> = (data: T) => R;
export type ErrorStateMapper<R> = (error: unknown) => R;

export interface Mappers<T, P, S, E> {
  readonly pendingState?: PendingStateMapper<P>;
  readonly successState: SuccessStateMapper<T, S>;
  readonly errorState: ErrorStateMapper<E>;
}

export function fromSuspenseState<T, P, S, E>(mappers: Mappers<T, P, S, E> & { pendingState: PendingStateMapper<P> }): OperatorFunction<SuspenseState<T>, P | S | E>;
export function fromSuspenseState<T, P, S, E>(mappers: Mappers<T, P, S, E> & { pendingState?: undefined }): OperatorFunction<SuspenseState<T>, null | S | E>;
export function fromSuspenseState<T, P, S, E>(mappers: Mappers<T, P, S, E>): OperatorFunction<SuspenseState<T>, P | S | E | null> {
  return (source$: Observable<SuspenseState<T>>): Observable<P | S | E | null> => {
    return source$.pipe(
      map((state) => {
        switch (state.type) {
          case "PENDING":
            return mappers.pendingState ? mappers.pendingState() : null;
          case "SUCCESS":
            return mappers.successState(state.data);
          case "ERROR":
            return mappers.errorState(state.error);
        }
      })
    );
  };
}

There is a solution using type inference based on property types. The code is provided to demonstrate the approach.

If you have faced similar situations or have suggestions, feel free to share your experience.

Answer №1

There appear to be various ways to tackle the issue at hand.


Approach 1:

In this method, conditional types are employed within PendingStateMapperResult, SuccessStateMapperResult, and ErrorStateMapperResult.

type PendingStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M extends { readonly pendingState: PendingStateMapper<infer R> } ? R : null;
type SuccessStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M extends { readonly successState: SuccessStateMapper<T, infer R> } ? R : T;
type ErrorStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M extends { readonly errorState: PendingStateMapper<infer R> } ? R : unknown;

export type FromSuspenseStateResult<T, P, S, E, M extends Mappers<T, P, S, E>> =
  | PendingStateMapperResult<T, P, S, E, M>
  | SuccessStateMapperResult<T, P, S, E, M>
  | ErrorStateMapperResult<T, P, S, E, M>;

export function fromSuspenseState<T, P, S, E, M extends Mappers<T, P, S, E> = Mappers<T, P, S, E>>(
  mappers: M
): OperatorFunction<SuspenseState<T>, FromSuspenseStateResult<T, P, S, E, M>> {
  return (source$: Observable<SuspenseState<T>>): Observable<FromSuspenseStateResult<T, P, S, E, M>> => {
    return source$.pipe(
      map((state) => {
        switch (state.type) {
          case "PENDING":
            return (mappers.pendingState?.() ?? null) as PendingStateMapperResult<T, P, S, E, M>;
          case "SUCCESS":
            return (mappers.successState?.(state.data) ?? state.data) as SuccessStateMapperResult<T, P, S, E, M>;
          case "ERROR":
            return (mappers.errorState?.(state.error) ?? state.error) as ErrorStateMapperResult<T, P, S, E, M>;
        }
      })
    );
  };
}

Approach 2:

Similar to the first method, this one also employs conditional types within PendingStateMapperResult, SuccessStateMapperResult, and ErrorStateMapperResult, but takes a different syntactical approach.

type PendingStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M["pendingState"] extends PendingStateMapper<infer R> ? R : null;
type SuccessStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M["successState"] extends SuccessStateMapper<T, infer R> ? R : T;
type ErrorStateMapperResult<T, P, S, E, M extends Mappers<T, P, S, E>> = M["errorState"] extends ErrorStateMapper<infer R> ? R : unknown;

export type FromSuspenseStateResult<T, P, S, E, M extends Mappers<T, P, S, E>> =
  | PendingStateMapperResult<T, P, S, E, M>
  | SuccessStateMapperResult<T, P, S, E, M>
  | ErrorStateMapperResult<T, P, S, E, M>;

export function fromSuspenseState<T, P, S, E, M extends Mappers<T, P, S, E> = Mappers<T, P, S, E>>(
  mappers: M
): OperatorFunction<SuspenseState<T>, FromSuspenseStateResult<T, P, S, E, M>> {
  return (source$: Observable<SuspenseState<T>>): Observable<FromSuspenseStateResult<T, P, S, E, M>> => {
    return source$.pipe(
      map((state) => {
        switch (state.type) {
          case "PENDING":
            return (mappers.pendingState?.() ?? null) as PendingStateMapperResult<T, P, S, E, M>;
          case "SUCCESS":
            return (mappers.successState?.(state.data) ?? state.data) as SuccessStateMapperResult<T, P, S, E, M>;
          case "ERROR":
            return (mappers.errorState?.(state.error) ?? state.error) as ErrorStateMapperResult<T, P, S, E, M>;
        }
      })
    );
  };
}

Approach 3:

This option draws inspiration from the conversation in the comments with "jcalz". It makes use of generic parameter defaults.

export function fromSuspenseState<T, P = null, S = T, E = unknown>(mappers: Mappers<T, P, S, E>): OperatorFunction<SuspenseState<T>, P | S | E> {
  return (source$: Observable<SuspenseState<T>>): Observable<P | S | E> => {
    return source$.pipe(
      map((state) => {
        switch (state.type) {
          case "PENDING":
            return (mappers.pendingState?.() ?? null) as P;
          case "SUCCESS":
            return (mappers.successState?.(state.data) ?? state.data) as S;
          case "ERROR":
            return (mappers.errorState?.(state.error) ?? state.error) as E;
        }
      })
    );
  };

Regrettably, each approach requires a type cast. This seems to be a current necessity due to an existing issue.

One possible solution to avoid the type cast could involve utilizing overload signatures, although this might result in an excessive number of function overloads to cover all variants.

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

Is there a way to verify whether a key within an Interface corresponds to a class that is a subclass of a specific Parent

Is there a method in typescript to ensure that a property in an interface must be of type "child subclass C, which extends class P"? example.ts import { P } from '/path/to/types' class C extends P { ... } types.ts // `C` cannot be accessed ...

Issue: An object with keys {} is not suitable as a React child, causing an error

I am new to TypeScript and seeking help from the community. Currently, I am working on a to-do list project where I am using React and TypeScript together for the first time. However, I encountered an error that I cannot decipher. Any assistance would be g ...

Issue encountered while trying to iterate through an observable: Object does not have the capability to utilize the 'forEach' property or method

I am currently following the pattern outlined in the hero.service.ts file, which can be found at this link: https://angular.io/docs/ts/latest/guide/server-communication.html The Observable documentation I referenced is available here: When examining my c ...

Creating a method to emphasize specific words within a sentence using a custom list of terms

Looking for a way to highlight specific words in a sentence using TypeScript? We've got you covered! For example: sentence : "song nam person" words : ["song", "nam", "p"] We can achieve this by creating a custom pipe in typescript: song name p ...

Typescript - Defining string value interfaces

I have a property that can only be assigned one of four specific strings. Currently, I am using a simple | to define these options. However, I want to reuse these types in other parts of my code. How can I create an interface that includes just these 4 va ...

Validating dynamic textboxes in Angular with custom rules

Creating dynamic textboxes with *ngFor in Angular <tr *ngFor="let computer in _Computers; let i = index;"> <td>{{computer.Name}}</td><td>{{computer.Optional}}</td> <td> <input matInput [formControl] = " ...

Exploring ways to conduct a thorough scan of object values, inclusive of nested arrays

My goal is to extract all values from an object. This object also includes arrays, and those arrays contain objects that in turn can have arrays. function iterate(obj) { Object.keys(obj).forEach(key => { console.log(`key: ${key}, value: ${o ...

Encountering a problem while attempting to host an Angular application on localhost:4200

After executing the ng serve command, I encountered an issue in the browser: An error occurred while trying to resolve "localhost:4200" ("") for "10.238.0.0": rpc error: code = Unknown desc = no such record I apologize if this question seems basic, as I ...

Having difficulty launching a TypeScript Node.js application using ts-node and pm2

I have a node app built with TypeScript and it works fine with nodemon, but when I try to move it to the server using PM2, I encounter errors. I've searched GitHub and StackOverflow for solutions, but nothing has worked for me so far. Any help would b ...

What is the best way to include an object in a document before sending it back?

I am facing a challenge that involves retrieving data from Firestore using angularfire. Once I fetch a collection, I need to iterate through each document in the collection and then make a separate request to Firestore to get additional document values. Th ...

Angular and WEB API experiencing issues with the update function synchronization

Currently, I'm developing a CRUD example using dotnet core and Angular. In the backend, I have implemented a function in the CarController.cs as shown below: CarController.cs [Route("UpdateCar")] [HttpPut] public IActionResult Put([ ...

How to dynamically set a background image using Ionic's ngStyle

I've been trying to set a background image on my card using ngStyle Take a look at my code below: <ion-slides slidesPerView="1" centeredSlides (ionSlideWillChange)= "slideChange($event)" [ngStyle]= "{'background-image': 'ur ...

Explore one of the elements within a tuple

Can we simplify mapping a tuple element in TypeScript? I'm seeking an elegant way to abstract the following task const arr: [string, string][] = [['a', 'b'], ['c', 'd'], ['e', 'f']] const f ...

Looking for a way to dynamically append a child element within another child

Struggling to include a new child within a specific child in Json myObject:any[] = []; this.myObject = { "type": "object", "properties": { "first_name": { "type": "string" }, "last_name": { "type": "string" }, } } addF ...

Tips for hiding a bootstrap modal in Angular4 when it has no content

I am currently working on an Angular 4 e-commerce application. One of the requirements is to hide a bootstrap modal when there are no products in the cart. When a user selects some products, they are added to the mycart modal screen. The user also has the ...

Having trouble getting the onClick function to work in your Next.js/React component?

Recently, I delved into using next-auth for the first time and encountered an issue where my login and logout buttons' onClick functions stopped working when I resumed work on my project the next day. Strangely, nothing is being logged to the console. ...

`Firebase User Instance and Custom Firestore Document`

Recently, I posted a question regarding Google Firebase Angular Firestore switchMap and encountered some issues. The question can be found here. After exploring AngularFireAuth, I learned that it is used to create a User object with fixed values, requirin ...

Error message: "Declared app-routing module in Angular 2 is encountering routing declaration

Currently, I am immersing myself in learning Angular 2 through the official tutorial available at https://angular.io/docs/ts/latest/tutorial/toh-pt5.html. However, I have encountered an issue related to routing. The error message displayed states: Type Das ...

Ways to carry out two distinct actions following a successful service call with NGRX

I am currently working on a product search feature. The process involves dispatching a 'search_start' action with specific parameters for the search. This action triggers an effect that communicates with a service to retrieve a list of products b ...

Steps for importing a CommonJS module that exports as a callable into TypeScript

I am dealing with a project that has a mixture of JavaScript and TypeScript files. Within the project, there is a JS library that follows this structure: module.exports = () => { // logic dependent on environment variables // then... return { ...