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

Leverage the compiler API to perform type inference

Exploring TypeScript's compiler API for basic type inference has proven to be a challenge with limited helpful information found in documentation or online searches. My goal is to create a function inferType that can determine and return the inferred ...

having difficulty interpreting the information from angular's httpclient json request

After creating an Angular function in typescript to make an http request for JSON data and save it to an object, I noticed that the function requires two clicks of the associated button to work properly. Although the connection and data parsing are success ...

Derive data type details from a string using template literals

Can a specific type be constructed directly from the provided string? I am interested in creating a type similar to the example below: type MyConfig<T> = { elements: T[]; onUpdate: (modified: GeneratedType<T>) => void; } const configur ...

Can you explain the distinction between Observable<ObservedValueOf<Type>> versus Observable<Type> in TypeScript?

While working on an Angular 2+ project in Typescript, I've noticed that my IDE is warning me about the return type of a function being either Observable<ObservedValueOf<Type>> or Observable<Type>. I tried looking up information abou ...

Utilizing the WebSocket readyState to showcase the connection status on the application header

I am currently in the process of developing a chat widget with svelte. I aim to indicate whether the websocket is connected or not by utilizing the websocket.readyState property, which has the following values: 0- Connecting, 1- Open, 2- Closing, 3- Close ...

Disabling FormArray on-the-fly in Angular

I have a scenario where I need to disable multiple checkboxes in a FormArray when the page loads. Despite my attempts to implement this, it hasn't been successful so far. Can someone provide guidance on how to achieve this? .ts file public myForm: Fo ...

Passing a custom data type from a parent component to a child component in React

I'm currently working on developing a unique abstract table component that utilizes the MatTable component. This abstract table will serve as a child element, and my goal is to pass a custom interface (which functions like a type) from the parent to t ...

Issue with Pagination functionality when using Material-UI component is causing unexpected behavior

My database retrieves data based on the page number and rows per page criteria: const { data: { customerData: recent = null } = {} } = useQuery< .... //removed to de-clutter >(CD_QUERY, { variables: { input: { page: page, perPag ...

Guide to customizing Material UI theme using Typescript in a separate file

Trying to customize Material UI theme overrides can be a bit tricky, as seen in the example below: // theme.ts const theme: Theme = createMuiTheme({ overrides: { MuiButton: { root: { display: 'inline-block', fontWeigh ...

Encountered issue when attempting to insert items into the list in EventInput array within FullCalendar and Angular

I am struggling to create a dynamic object that I need to frame and then pass to the FullCalendar event input. Here is my initial object: import { EventInput } from '@fullcalendar/core'; ... events: EventInput[]; this.events = [ { title: &ap ...

Is there a way for me to showcase a particular PDF file from an S3 bucket using a custom URL that corresponds to the object's name

Currently, I have a collection of PDFs stored on S3 and am in the process of developing an app that requires me to display these PDFs based on their object names. For instance, there is a PDF named "photosynthesis 1.pdf" located in the biology/ folder, and ...

merging JavaScript objects with complex conditions

I am attempting to combine data from two http requests into a single object based on specific conditions. Take a look at the following objects: vehicles: [ { vId: 1, color: 'green', passengers: [ { name: 'Joe', ag ...

What is the method for dynamically assigning a name to ngModel?

I have the following code snippet: vmarray[]={'Code','Name','Place','City'} export class VMDetail { lstrData1:string; lstrData2:string; lstrData3:string; lstrData4:string; lstrData5:string; ...

A script object must only permit one property at a time

I am unfamiliar with TypeScript and I have an object named obj with 3 properties: a, b, c. However, it is important to note that b and c cannot exist together in the same object. So, my object will either be: obj = { a: 'xxx', b: 'x ...

Having trouble accessing the theme in a styled component with @emotion/styled

https://i.stack.imgur.com/zHLON.png I've been using @emotion/react for theming and successfully injected the theme into it. I can access the theme using useTheme within components, but I'm facing some difficulties in accessing the theme within s ...

Module 'ngx-bootstrap' not found in project

My application is encountering an issue with ngx-bootstrap where the module can no longer be detected unless the path is specified. For instance: import { BsModalService, BsModalRef } from 'ngx-bootstrap'; results in "Cannot find module ' ...

Developing middleware for managing event handlers

Scenario: I am tasked with managing multiple events that necessitate an "available client". Therefore, in each event handler, my first step is to attempt to acquire an available client. If no client is available, I will send a "Service unavailable" messag ...

I am looking to update my table once I have closed the modal in Angular

I am facing an issue with refreshing the table in my component using the following function: this._empresaService.getAllEnterprisePaginated(1);. This function is located in my service, specifically in the modal service of the enterprise component. CODE fo ...

When working with Typescript, you can declare an interface and split its definition across multiple files

I've been developing a software application that utilizes WebSocket with the NodeJS ws package. My networking structure revolves around a module responsible for handling message reception and transmission. Given that I'm working with TypeScript, ...

Utilizing TypeScript path aliases in a Create React App project with TypeScript and ESLint: A step-by-step guide

I utilized a template project found at https://github.com/kristijorgji/cra-ts-storybook-styled-components and made some enhancements. package.json has been updated as follows: { "name": "test", "version": "0.1.0" ...