Nate Radebaugh
Nate Radebaugh

Reputation: 1084

Can typescript infer a function's response type based on parameter values?

I have an API with that takes a request object to get a list of records. If inlineCount is set on the request object, the API returns { results: T[], inlineCount: number }. Otherwise, T[] is returned.

This looks something like:

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

interface Options {
  inlineCount?: boolean;
}

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

async getClients(options: Options) {
  return this.getApiResponse<model.Client>(options);
}

Here's what I'm hoping to have happen:

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

However, the type of both calls is model.Client[] | inlineCountResult<model.Client> which makes my code require unnecessary type castings to work properly. I understand I can do the following, but I'd like to avoid the as casting:

const clients: Client[] = await (getClients(clientQueryOptions) as Promise<Client[]>);

One idea I had is to write a function that will return one of the return types as one of the args, but I'd like to get by without the extra arg being required.

Partial solution:

//
// 2 "normalize" functions each with a single return type
//
export function normalizeArray<T>(result: T[] | inlineCountResult<T>): T[] {
    return Array.isArray(result) ? result : result.results;
}

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

//
// Types for request
//
export interface ExecuteQueryRequest {
    inlineCount?: boolean;
}
export type QueryResponseNormalizer<T, TResponse> = (response: (T[] | inlineCountResult<T>)) => TResponse;

//
// Query method to handle
//
function executeQuery<T, TResponse>(request: ExecuteQueryRequest, normalizeResults: QueryResponseNormalizer<T, TResponse>): TResponse {
    const items: T[] = []; // eg 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);

Upvotes: 1

Views: 1669

Answers (2)

Nate Radebaugh
Nate Radebaugh

Reputation: 1084

Oblosys's answer helped me get to a solution that allows my ideal/simple API surface without needing any extra args/types in my callers other than 2 distinct Options vs InlineCountOptions types with and without the inlineCount bool:

TypeScript playground

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

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

Full Solution

Setup

Define entities for data coming from the API

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

Define 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, eg 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 for each entity/model

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

Usage

Array request

const clientQueryOptions = queryOptions({});

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

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

Inline count request

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);

Upvotes: 0

Oblosys
Oblosys

Reputation: 15116

With conditional types it's possible to have the result type depend on the presence of inlineCount in the parameter, and also infer the type of the result array from the normalize function. Disregarding the promises for simplicity, you can define 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[] 

and type getClients like this:

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

If you now define two dummy normalizers like these

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

the types of the responses will be inferred correctly and incorrectly-typed normalizers will yield 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

You'll need to tweak it a bit to account for the promises, and maybe other option properties besides inlineCount, but this should give you a rough idea of a solution.

Upvotes: 2

Related Questions