A versatile generic type infused with dynamic typing and optional parameter flexibility

Looking to develop a function that can accept an optional errorCallback parameter. In cases where the consumer of this function does not provide a callback, I aim to default to a preset handler. The key criteria here are strong typing and utilizing the return type defined by the consumer's custom errorCallback, falling back on my tailored error handler's return type when necessary.

Below you'll find the code snippet:

export enum ErrorType {
    VALIDATION_ERROR,
    API_ERROR,
    UNKNOWN_ERROR,
    UNAUTHORIZED
}

type MockZodType ={}

type MockResponseError = {}

export type ServiceError<TSchema extends MockZodType> =
    | {
            type: ErrorType.VALIDATION_ERROR;
            message: string;
            status: number;
            data: TSchema;
            originalError: MockResponseError;
      }
    | {
            type: ErrorType.API_ERROR;
            message: string;
            status: number;
            data: unknown;
            originalError: MockResponseError;
      }
    | {
            type: ErrorType.UNKNOWN_ERROR;
            message: string;
            status: number;
            data: unknown;
            originalError: unknown;
      }
    | {
            type: ErrorType.UNAUTHORIZED;
            message: string;
            status: number;
            data: unknown;
            preventDefaultHandler: boolean;
            originalError: MockResponseError;
      };

export type ServiceCallOptions<
    TServiceCallReturn extends Promise<unknown>,
    TSchema extends MockZodType = never,
    TErrorCallbackReturn extends Promise<unknown> = Promise<ServiceError<TSchema>>
> = {
    serviceCall: () => TServiceCallReturn;
    errorSchema?: TSchema;
    errorCallback?: (e: ServiceError<TSchema>) => TErrorCallbackReturn;
};

export async function callService<
    TServiceCallReturn extends Promise<unknown>,
    TSchema extends MockZodType = never,
    TErrorCallbackReturn  extends Promise<unknown> = Promise<ServiceError<TSchema>>
>({
    serviceCall,
    errorSchema,
    errorCallback = async (e)=> {
        if (e.type === ErrorType.UNAUTHORIZED && !e.preventDefaultHandler) {
            // some custom default handler;
        }
        return e; 
    }
}: ServiceCallOptions<TServiceCallReturn, TSchema, TErrorCallbackReturn>) {
    // some logic
}

An error emerges from the default value in the errorCallback parameter with the following message:

Type '(e: ServiceError<TSchema>) => Promise<ServiceError<TSchema>>' is not assignable to type '(e: ServiceError<TSchema>) => TErrorCallbackReturn'.
  Type 'Promise<ServiceError<TSchema>>' is not assignable to type 'TErrorCallbackReturn'.
    'Promise<ServiceError<TSchema>>' is assignable to the constraint of type 'TErrorCallbackReturn', but 'TErrorCallbackReturn' could be instantiated with a different subtype of constraint 'Promise<unknown>'.

No interest in using overloads, so why does it fail to work as expected?

To test or revise the code, check out the ts playground.

Answer №1

The issue lies in the fact that when a generic function is called, it is the caller who specifies the type arguments, not the function writer. Although the compiler can often infer the types, it does so on behalf of the caller and not the implementer. The implementer may set default type arguments for cases where inference is not possible, but the caller always has the option to manually define the type arguments based on their constraints.

Even if your intention is for the compiler to use

Promise<ServiceError<TSchema>>
as the type argument for TErrorCallbackReturn when the errorCallback property is omitted by the caller, this cannot be guaranteed. A caller could bypass this logic by explicitly specifying unexpected type arguments like so:

const f = await callService<
    Promise<string>, {}, Promise<number>
>({ serviceCall: async () => "a" });

In this scenario, the caller is asserting that the missing errorCallback property would return a number, which contradicts the expected behavior defined by the default argument. This mismatch results in a compilation error stating that 'Promise<ServiceError<TSchema>>' is assignable to 'TErrorCallbackReturn', but there exists a potential subtype conflict.


To address this issue, one approach is to utilize function overloads, providing explicit control over the available call signatures visible to the caller. By doing this, passing the third type argument without also providing the expected errorCallback property becomes impossible.

async function callService<
    TServiceCallReturn extends Promise<unknown>,
    TSchema extends MockZodType = never,
>(arg: {
    serviceCall: () => TServiceCallReturn, errorSchema?: TSchema,
    errorCallback?: never
}): Promise<void>;

async function callService<
    TServiceCallReturn extends Promise<unknown>,
    TSchema extends MockZodType = never,
    TErrorCallbackReturn extends Promise<unknown> = Promise<ServiceError<TSchema>>
>(arg: {
    serviceCall: () => TServiceCallReturn, errorSchema?: TSchema,
    errorCallback: (e: ServiceError<TSchema>) => TErrorCallbackReturn
}): Promise<void>;

If significant refactoring is not desired, another solution is to acknowledge the unlikely nature of such invalid calls and assert the correct type for the default callback argument. An example of this approach might look like:

async function callService<
    TServiceCallReturn extends Promise<unknown>,
    TSchema extends MockZodType = never,
    TErrorCallbackReturn extends Promise<unknown> = Promise<ServiceError<TSchema>>
>({
    serviceCall,
    errorSchema,
    errorCallback = (async (e: ServiceError<TSchema>) => {
        if (e.type === ErrorType.UNAUTHORIZED && !e.preventDefaultHandler) {
            // some custom default handler;
        }
        return e;
    }) as (e: ServiceError<TSchema>) => TErrorCallbackReturn
}: ServiceCallOptions<TServiceCallReturn, TSchema, TErrorCallbackReturn>) { }

This adjustment resolves the compiler error, provided the assertion remains accurate, ensuring the functionality behaves as intended.

Playground link to 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

How can we define a function using a generic type in this scenario using Typescript?

Here's a challenge that I'm facing. I have this specific type definition: type FuncType<T> = (value: T) => T I want to create a function using this type that follows this structure: const myFunc: FuncType<T> = (value) => valu ...

Issue with executing a server-side function in a Next.js application

I'm encountering an issue with my Next app. I have a method in my ArticleService class that retrieves all articles from my SQL database. async getArticles(): Promise<IArticle[] | ServiceError> { try { const reqArticles = await sql< ...

Challenges faced with the Nativescript Software Development Kit

I am currently working on a Nativescript app with Angular and using a JSON server. However, I am facing some errors when I try to run 'tns run android' or 'tns doctor' commands. × The ANDROID_HOME environment variable is either not se ...

Achieving the incorporation of multiple components within a parent component using Angular 6

Within parent.component.html The HTML code I have implemented is as follows: <button type="button" class="btn btn-secondary (click)="AddComponentAdd()">Address</button> <app-addresse *ngFor="let addres of collOfAdd" [add]="addres">< ...

What sets apart using (@Inject(Http) http: Http) from not using it?

Following a recent query, I now have a new question. What sets apart these two approaches? Here is the original code I used: import {Http, HTTP_PROVIDERS} from 'angular2/http'; @Component({ viewProviders: [HTTP_PROVIDERS], ..// constructor(h ...

Can someone please point me in the right direction to locate a Typescript project within Visual Studio

I've been struggling with this issue for days and still can't find a solution. After installing the Typescript tool for Visual Studio 2015, it appears to be successfully installed. https://i.stack.imgur.com/nlcyC.jpg However, I am unable to loc ...

Transform object into data transfer object

Looking for the most efficient method to convert a NestJS entity object to a DTO. Assuming the following setup: import { IsString, IsNumber, IsBoolean } from 'class-validator'; import { Exclude } from 'class-transformer'; export clas ...

Issue: the module '@raruto/leaflet-elevation' does not include the expected export 'control' as imported under the alias 'L' . This results in an error message indicating the absence of exports within the module

Looking for guidance on adding a custom Leaflet package to my Angular application called "leaflet-elevation". The package can be found at: https://github.com/Raruto/leaflet-elevation I have attempted to integrate it by running the command: npm i @raruto/ ...

Typescript indicates that an object may be potentially null

I've hit a roadblock where I keep getting warnings that the objects might be null. After searching online and on StackOverflow, I've tried numerous solutions with no luck. My goal is to insert the text "test" into the HTML elements using their ID ...

"Displaying the Material Input TextBox in a striking red color when an error occurs during

After referring to the links provided below, I successfully changed the color of a textbox to a darkish grey. Link 1 Link 2 ::ng-deep .mat-form-field-appearance-outline .mat-form-field-outline { color: #757575!important; } Although this solved the ...

Saving a boolean value and a number to Firestore in an Angular application

In my Angular 5 typescript project, I have a form with various input fields and selections. Here is how I am capturing the form values: let locked: boolean = (<HTMLInputElement>document.getElementById("locked")).value; let maxPlayers: number = (& ...

"Learn how to pass around shared state among reducers in React using hooks, all without the need for Redux

I've built a React hooks application in TypeScript that utilizes multiple reducers and the context API. My goal is to maintain a single error state across all reducers which can be managed through the errorReducer. The issue arises when I try to upd ...

What is the reason behind Typescript errors vanishing after including onchange in the code?

When using VSCode with appropriate settings enabled, the error would be displayed in the following .html file: <!DOCTYPE html> <html> <body> <div> <select> </select> </div> <script&g ...

Issues arise with Typescript Intellisense in Visual Studio Code causing it to stop functioning

I'm currently exploring the world of building desktop applications using Electron and Typescript. After selecting Visual Studio Code as my IDE, everything was going smoothly and I managed to successfully load a sample HTML file into Electron. However ...

Encountering issues while retrieving date data from Firebase in Angular 6

this.qS = this.afDatabase.list('path', ref => { return ref.limitToLast(1000); }).snapshotChanges().map(changes => { return changes.map(c => ({ key1: c.payload.key,value1:c.payload.val() })); }); this.qS.subscribe(values =&g ...

Importing TypeScript modules dynamically can be achieved without the need for Promises

I find myself in a scenario where the dynamic nature of these commands is crucial to prevent excessive loading of unnecessary code when executing specific command-line tasks. if (diagnostics) { require('./lib/cli-commands/run-diagnostics').run ...

Creating a seamless integration between Angular 2's auth guard and observables to enhance application security

I'm having trouble setting up an auth guard for one of my routes because I am unsure how to implement it with an observable. I am using ngrx/store to store my token. In the guard, I retrieve it using this.store.select('auth'), which returns ...

Unexpected token error on an optional property in Visual Studio Code

I encountered a problem with a project I cloned. Below is the code snippet created using this project: https://github.com/enuchi/React-Google-Apps-Script export interface Vehicle { wheels: number; insurance?: string; } export default class Car { whe ...

The useRef function is malfunctioning and throwing an error: TypeError - attempting to access 'filed2.current.focus' when 'filed2' is null

I'm attempting to switch focus to the next input field whenever the keyboard's next button is pressed. After referring to the react native documentation, it seems that I need to utilize the useRef hook. However, when following the instructions f ...

Constructing an Angular 2 application using a solo TypeScript file that is compiled individually

At the moment, I am in the process of developing a Chrome Extension using Angular 2. The application includes a background.js file which handles the functionality for a long-running task that operates while the extension is active. Currently, this backgrou ...