Home > Net >  How to change functions return type based on the input arguments in Typescript?
How to change functions return type based on the input arguments in Typescript?

Time:03-11

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 if transformResult is not provided.
  • Returns TResult (or return type of transformResult 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' }),
});

Playground

  • Related