Home > OS >  TypeScript conditional type based on optional object property
TypeScript conditional type based on optional object property

Time:11-09

This is a simplified version of my real issue

I have an external API similar to this:

interface CommonRequestParams {
    user: {};
}

function f1(params: CommonRequestParams) {/* do something */}

interface SecondRequestParams extends CommonRequestParams {
    payload: { id: string };
}

function f2(params: SecondRequestParams) {/* do something */}

interface ThirdRequestParams extends CommonRequestParams {
    payload?: { id: number };
}

function f3(params: ThirdRequestParams) {/* do something */}

const api = {
    f1,
    f2,
    f3,
};

I created a wrap function to provide common data for method calls.

My problem is with the types, when dealing with optional properties (payload can be optional)

see "problematic part" marked with a comment

// api methods could be used like this:
// f1({ user: {} });
// f2({ user: {}, payload: { id: "1" } });
// f3({ user: {} });
// f3({ user: {}, payload: { id: 1 } });

type RequestApi = typeof api;

type RequestApiMethod = RequestApi[keyof RequestApi];

type RequestApiMethodPayload<M extends RequestApiMethod, P = Parameters<M>[0], > =
    // I think this is the problematic part
    P extends CommonRequestParams & { payload?: any }
        ? P["payload"]
        : never;

function wrap<M extends RequestApiMethod, >(method: M) {
    return (payload: RequestApiMethodPayload<M>) => {
        return method({
            user: {},
            payload,
        });
    };
}

Expected results commented below

const _f1 = wrap(f1);
const _f2 = wrap(f2);
const _f3 = wrap(f3);

_f1(); // this should be OK
_f2({ id: "1" }); // OK
_f3(); // this should be OK
_f3({ id: 1 }); // OK

_f1("unknown"); // this should NOT be ok

( I tried with more conditions / using infer { payload?: infer Payload }... ) but no success

CodePudding user response:

You only need to check if the payload key exists in P:

type RequestApiMethodPayload<M extends RequestApiMethod, P = Parameters<M>[0]> = "payload" extends keyof P ? P["payload"] : undefined;

This is similar to the in operator in JavaScript ("payload" in P).

Then in the return type you would check if this type includes undefined, and if it does, make the parameter optional:

function wrap<M extends RequestApiMethod>(method: M):
    undefined extends RequestApiMethodPayload<M>
        ? (params?: RequestApiMethodPayload<M>) => ReturnType<M>
        : (params: RequestApiMethodPayload<M>) => ReturnType<M>;

However, you'll get an error on the line that adds the payload... you could try an assertion but I just ignored it. Anyways, it works as expected now.

Playground

  • Related