I am trying to type a function, which takes an optional transformResult
. I have a problem typing the useQuery
function correctly, so it:
- Returns
QResult
type iftransformResult
is not provided. - Returns
TResult
(or return type oftransformResult
callback, but I am not sure if this is possible) if the callback is provided.
How can I achieve this? I don't mind using functions override if it can solve my problem easily, however conditional types are preferred.
Thanks a lot in advance!
type QueryOptions<QResult, TResult> = {
transformResult?: (queryResult: QResult) => TResult;
// Other options that are not so important
enableDebug?: boolean;
};
function useQuery<QResult, TResult = QResult>(
query: string,
options?: QueryOptions<QResult, TResult>,
) {
const queryResult = doSomeServerStuff<QResult>(query);
if (options?.transformResult != null) {
return options.transformResult(queryResult);
} else {
return queryResult;
}
}
function doSomeServerStuff<Q>(query: string): Q {
/* Not so important just to make it compile */
return {} as Q;
}
//typeOf 'number' <- Good
const result = useQuery<number>('q');
// Expecting 'boolean' type, but 'number | boolean' is actual
const transformedResult = useQuery<number, boolean>('q', {
transformResult: (qResult) => qResult != null,
});
Link to playground
CodePudding user response:
Function overloads shine here. When the return type of a function changes depending on the type if its argument, function overloads are nearly always the easiest way to implement that.
To start off I'd organize the type like so:
interface QueryOptionsBase {
// Other irrelevant options
enableDebug?: boolean;
}
interface QueryOptionsWithoutTransform extends QueryOptionsBase {
transformResult?: undefined;
};
interface QueryOptionsWithTransform<QResult, TResult> extends QueryOptionsBase {
transformResult: (queryResult: QResult) => TResult;
};
type QueryOptions<QResult, TResult> = QueryOptionsWithoutTransform | QueryOptionsWithTransform<QResult, TResult>
QueryOptionsBase
is the properties that all paths have.
QueryOptionsWithoutTransform
is the simple branch that has no transform.
QueryOptionsWithTransform
is the branch that has the transform
QueryOptions
is the union of both branches.
Then you implement the overloads like so:
function useQuery<QResult>(query: string, options?: QueryOptionsWithoutTransform): QResult
function useQuery<QResult, TResult = QResult>(query: string, options: QueryOptionsWithTransform<QResult, TResult>): TResult
function useQuery<QResult, TResult = QResult>(
query: string,
options?: QueryOptions<QResult, TResult>,
) {
const queryResult = doSomeServerStuff<QResult>(query);
if (options?.transformResult != null) {
return options.transformResult(queryResult);
} else {
return queryResult;
}
}
Not this works as expected:
// 'boolean' type
const transformedResult = useQuery<number, boolean>('q', {
transformResult: (qResult) => qResult != null,
});
But also this:
// '{ foo: string; }' type
const transformedResultInferred = useQuery('q', {
transformResult: (qResult) => { foo: 'bar' }),
});