Reputation: 1084
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
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:
// model.Client[]
const clients = await getClients();
// or
const clients2 = await getClients({ sort: "clientName" });
// InlineCountResult<model.Client>
const response = await getClients({ inlineCount: true });
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;
}
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);
}
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
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
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