Is it possible for TypeScript to deduce a function's return type from the values of its parameters?

In my API, a request object is used to retrieve a list of records. If the request object includes the property inlineCount, the API will return the data in the format

{ results: T[], inlineCount: number }
. Otherwise, it will simply return T[].

The code snippet below demonstrates this process:

interface InlineCountResult<T> {
    results: T[];
    inlineCount: number;
}

interface Options {
  inlineCount?: boolean;
}

async function getApiResponse<T>(options: Options): T[] | InlineCountResult<T> {
  return this.http.get(options);
}

async function getClients(options: Options) {
  return getApiResponse<model.Client>(options); // Returns model.Client[] or InlineCountResult<model.Client>
}

The goal is to have two different types of responses based on whether inlineCount is set or not:

let responseAsArray: model.Client[] = getClients();
let responseWithCount: InlineCountResult<model.Client> = getClients({ inlineCount: true });

However, both calls currently return the type

model.Client[] | InlineCountResult<model.Client>
, which requires unnecessary type casting for proper functionality. Although using as casting is an option, it's preferable to avoid it.

To address this issue, one approach is to create a function that can determine the return type without requiring an extra argument.

Partial solution:

// Functions to normalize response types
export function normalizeArray<T>(result: T[] | InlineCountResult<T>): T[] {
    return Array.isArray(result) ? result : result.results;
}

export function normalizeInlineCount<T>(result: T[] | InlineCountResult<T>) {
    return Array.isArray(result) ? new InlineCountResult<T>(result, result.length) : result.inlineCount ? result : new InlineCountResult<T>([], 0);
}

// Request types
export interface ExecuteQueryRequest {
    inlineCount?: boolean;
}
export type QueryResponseNormalizer<T, TResponse> = (response: (T[] | InlineCountResult<T>)) => TResponse;

// Query method handling
function executeQuery<T, TResponse>(request: ExecuteQueryRequest, normalizeResults: QueryResponseNormalizer<T, TResponse>): TResponse {
    const items: T[] = []; // Example: Call HTTP service

    return normalizeResults(items);
}

// One API for both scenarios
function getClients<TResponse>(request: ExecuteQueryRequest, normalizeResults: QueryResponseNormalizer<model.Client, TResponse>) {
    let response = executeQuery(request as ExecuteQueryRequest, normalizeResults);
    return response;
}

// Example calls
let responseA: InlineCountResult<model.Client> = getClients({ inlineCount: true }, normalizeInlineCount);
let responseB: model.Client[] = getClients({}, normalizeArray);

Answer №1

By utilizing conditional types, the result type can now depend on whether or not inlineCount is present in the parameter. Additionally, the type of the result array can be inferred from the normalize function. Simplifying things by overlooking promises, you can create these helper types:

interface InlineCountResult<T> {
  results: T[]
  inlineCount: number
}

interface InlineCountOption {
  inlineCount: boolean
}

type PlainNormalizer<T> = (res: T[]) => T[]

type InlineCountNormalizer<T> = (res: InlineCountResult<T>) => InlineCountResult<T>

type Normalizer<O, T> =
  O extends InlineCountOption ? InlineCountNormalizer<T> : PlainNormalizer<T>

type Result<O, T> = O extends InlineCountOption ? InlineCountResult<T> : T[]

Define the getClients type like this:

function getClients<O, T>(options: O, normalizer: Normalizer<O, T>): Result<O, T> {
  return {} as any
}

If two dummy normalizers are defined as follows:

const normalizeInlineCount: InlineCountNormalizer<number> = {} as any
const normalizeArray: PlainNormalizer<string> = {} as any

The response types will be correctly inferred and using incorrectly-typed normalizers will result in type errors:

const responseA = getClients({ inlineCount: true }, normalizeInlineCount)
const responseB = getClients({}, normalizeArray)
const responseC = getClients({ inlineCount: true }, normalizeArray) // type error
const responseD = getClients({}, normalizeInlineCount) // type error

TypeScript playground

A few adjustments may be required to accommodate promises and other option properties beside inlineCount, but this serves as a basic solution.

Answer №2

Oblosys's solution guided me to a resolution that simplifies my API interface, eliminating the need for additional arguments or types in the caller functions. Now, I only require two distinct types - Options and InlineCountOptions with and without the boolean inlineCount:

Explore TypeScript playground

// model.Client[]
const clients = await getClients();
// or
const clients2 = await getClients({ sort: "clientName" });

// InlineCountResult<model.Client>
const response = await getClients({ inlineCount: true });

Complete Solution

Initial Setup

Definition of entities for incoming API data

namespace model {
  export interface Client {
    clientName: string;
  }
}

Definition of request/response interfaces

interface Options {
  filter?: any;
  skip?: number;
  page?: number;
  sort?: string;
}

interface InlineCountOption extends Options {
  inlineCount: boolean
}

interface InlineCountResult<T> {
  results: T[]
  inlineCount: number
}

type Option<O extends Options> = O extends InlineCountOption ? InlineCountOption : Options;
function queryOptions<O>(options: O): Option<O> {
  return options as any;
}

Methods

Generic API caller, like fetch

type Result<O, T> = O extends InlineCountOption ? InlineCountResult<T> : T[] 
async function hitApi<O, T>(url: string, options: O): Promise<Result<O, T>> {
  let data: unknown = []; // ex

  return data as Result<O, T>;
}

Specific to each entity/model

function getClients<O>(options: O): Promise<Result<O, model.Client>> {
  return hitApi('getClients', options);
}

Utilization

Array request

const clientQueryOptions = queryOptions({});

// model.Client[]
const clients = await getClients(clientQueryOptions);

const clientNames = clients.map(x => x.clientName);

Request with inline count

const clientQueryOptions = queryOptions({ inlineCount: true });

// InlineCountResult<model.Client>
const clientsResponse = await getClients(clientQueryOptions);

const numClients = clientsResponse.inlineCount;
const clients = clientsResponse.results;
const clientNames = clients.map(x => x.clientName);

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

Leveraging the injectable service within the end callback function alongside interactJS

When using interactJS with Angular to enable drag and drop functionality for elements with the 'draggable' class, everything was working smoothly until I encountered an issue with using the injected service of the component in the callback functi ...

Utilizing a LoopBack4 Interceptor to access repositories within the application

I am looking to improve the functionality of an Interceptor within my LoopBack4 application. Currently, the Interceptor simply displays the start and end of a controller method call on the command line, as outlined in this documentation. Here is how my Lo ...

How to resolve: "Error: Cannot access 'Id' property of undefined" in Node.js using TypeScript

My code is giving me trouble. I am attempting to locate an element in an array using the following code snippet Name() { console.log(LoadItems.ItemConfigs); var ItemConfig = LoadItems.ItemConfigs.find(itemconf => itemconf.Id === this.ConfigId); ...

Enhance Summernote functionality by creating a custom button that can access and utilize

Using summernote in my Angular project, I am looking to create a custom button that can pass a list as a parameter. I want to have something like 'testBtn': this.customButton(context, listHit) in my custom button function, but I am unsure how to ...

What is the best way to organize an array both alphabetically and by the length of its elements?

Imagine I am working with an array like this: ['a', 'c', 'bb', 'aaa', 'bbb', 'aa']. My goal is to sort it in the following order: aaa, aa, a, bbb, bb, c. this.array= this.array.sort((n1, n2) => ...

Watch a live Youtube stream directly on Discord using discord.js!

Recently, I've been experimenting with creating a custom Discord bot as a way to have some fun and help out my friends. One of the features I'm trying to implement is streaming a live video in a voice chat whenever someone uses the !play study co ...

Unable to set values to properties of an object within an Angular service

The service seems to be encountering an issue with the "message" object. Even though property values are assigned using the "add()" method, accessing these properties directly from the component or through a getter still returns undefined values. This disc ...

Expanding Rows in a React Data Table: Utilizing Extra Props

Browsing through the documentation for React Data Table, it appears that there is a method to provide additional Props to an expandableRowsComponent. This is an excerpt from the documentation: expandableComponentProps With expandableComponentProps, you c ...

Updating the useState() function in React when the value changes can be done by utilizing the

I'm struggling to update the count value in my React project. Within my dynamic set, I aim to display the size of the set whenever it changes! My goal is to continuously update the count variable to match ratedSet.size whenever the set's size c ...

The output from the second request using RxJS

I am facing an issue with returning an Observable from the second request. Here is the scenario I am encountering: commandRequest(action:string, commandData:any):Observable<CashDesckResponse> { let command:CashDeskRequest; //ask my backend f ...

Custom typings for Next-Auth profile

I'm experiencing an issue with TypeScript and Next Auth type definitions. I followed the documentation guidelines to add my custom types to the NextAuth modules, specifically for the Profile interface in the next-auth.d.ts file. It successfully adds t ...

What is the best way to determine the property type dynamically in TypeScript based on the value of another property?

I have been working with Polymorphic relationships and currently have the following TypeScript interface defined: interface SubjectA {} interface SubjectB {} interface SubjectC {} enum SubjectType { SubjectA = 'Subject A', SubjectB = 'S ...

How can you retrieve the property value from an object stored in a Set?

Consider this scenario: SomeItem represents the model for an object (which could be modeled as an interface in Typescript or as an imaginary item with the form of SomeItem in untyped land). Let's say we have a Set: mySet = new Set([{item: SomeItem, s ...

What are the best ways to format text conditionally depending on a form's status?

Is there a way to change the text color in an HTML form to be red when the form is invalid and green when it is valid using Angular 8? (HTML) <p class="status"> Form Status: {{ Form.status }} </p> (TS) Form = this.fb.group({ ...

"In TypeScript, when something is undefined, it means its value

I am currently working on a class with code to help manage a database. export class DatabaseHelper { public browserID : number; private ConfigID = 17; } Within this class, I am attempting to access the value of ConfigID SetBrowserID() { ...

Guide to extracting the JSON array from a JSON object with Angular

In my angular application, I have made a call to the API and retrieved a JSON object in the console. However, within this JSON object, there are both strings and arrays. My task now is to extract and parse the array from the object in the console. The JSO ...

Utilizing a foundational element to automatically unsubscribe from multiple observable subscriptions

Within our Angular application, we have implemented a unique concept using a Base Component to manage observable subscriptions throughout the entire app. When a component subscribes to an observable, it must extend the Base Component. This approach ensures ...

What is preventing me from defining the widget as the key (using keyof) to limit the type?

My expectations: In the given scenario, I believe that the C component should have an error. This is because I have set the widget attribute to "Input", which only allows the constrained key "a" of type F. Therefore, setting the value for property "b" sho ...

Ionic: Automatically empty input field upon page rendering

I have an input field on my HTML page below: <ion-input type="text" (input)="getid($event.target.value)" autofocus="true" id="get_ticket_id"></ion-input> I would like this input field to be cleared eve ...

Developing a function that takes a parameter which can be used with or without an additional argument when invoked

In my React application, I have a method that accepts a parameter for displaying a modal. const displayModal = (p:Result) => { setConfirm(true); if(p) { //check variable for truthy setSelectedRow(p); } ...