sarneeh
sarneeh

Reputation: 1388

Make second parameter required/optional based on first parameter value

I have a function with 2 parameters and want the second parameter be optional/required based on the first parameter value.

Take a look at the code:

enum Endpoint {
    USERS = '/users/:userId',
    ORDERS = '/orders'
}

type EndpointParams = {
    [Endpoint.USERS]: 'userId';
    [Endpoint.ORDERS]: void;
}

type EndpointResponse = {
    [Endpoint.USERS]: any;
    [Endpoint.ORDERS]: any;
}

function callEndpoint(endpoint: Endpoint, params?: EndpointParams[typeof endpoint]): EndpointResponse[typeof endpoint] {
    return {};
}

callEndpoint(Endpoint.USERS); // should error
callEndpoint(Endpoint.USERS, 'param'); // should pass
callEndpoint(Endpoint.ORDERS); // should pass
callEndpoint(Endpoint.ORDERS, 'param'); // should error

I'd like the params to be required if there's a corresponding key/value in EndpointParams.

Is this possible and if it is, then how to implement it?

Upvotes: 2

Views: 2568

Answers (2)

Raymond_78
Raymond_78

Reputation: 126

So you can do it like this:

enum Endpoint {
    USERS = '/users/:userId',
    ORDERS = '/orders'
}

type UsersEndPoint = {
    endpoint: Endpoint.USERS;
    payload: {
      user_id: number,
      name: string
    };
}

type OrdersEndpoint = {
    endpoint: Endpoint.ORDERS;
}

type Endpoints = UsersEndPoint | OrdersEndpoint;

function callEndpoint<Endpoint extends Endpoints['endpoint']>(...args: Extract<Endpoints, {endpoint: Endpoint}> extends {payload: infer Payload} ? [endpoint: Endpoint, payload: Payload] : [endpoint: Endpoint]) {
    return {};
}

callEndpoint(Endpoint.USERS); // will error
callEndpoint(Endpoint.USERS, {user_id: 1, name: 'Firt namse'}); // will pass
callEndpoint(Endpoint.ORDERS); // will pass
callEndpoint(Endpoint.ORDERS, 'param'); // will error

The callEndpoint function is a generic function in TypeScript that takes one or more arguments. Let's break down the function signature:

<Endpoint extends Endpoints['endpoint']>: This is a generic type parameter named Endpoint that extends the endpoint property of the Endpoints type, which is a discriminated union type consisting of UsersEndPoint and OrdersEndpoint.

...args: Extract<Endpoints, {endpoint: Endpoint}> extends {payload: infer Payload} ? [endpoint: Endpoint, payload: Payload] : [endpoint: Endpoint]: This is a rest parameter args that represents the variable number of arguments passed to the function. It uses TypeScript's conditional types to determine the type of args based on the value of Endpoint:

Extract<Endpoints, {endpoint: Endpoint}> extracts the specific subtype of Endpoints that has an endpoint property matching the value of Endpoint. This is used to infer the type of the payload for that specific endpoint.

extends {payload: infer Payload} is a conditional type that checks if the extracted subtype has a payload property, and if so, infers the type of the payload property as Payload.

? [endpoint: Endpoint, payload: Payload] : [endpoint: Endpoint] is a ternary conditional type that specifies the return type of the function. If the Payload type is inferred (i.e., if the endpoint has a payload), the return type is a tuple with the endpoint and payload as its elements. Otherwise, if no Payload type is inferred (i.e., if the endpoint does not have a payload), the return type is a tuple with only the endpoint as its element.

Upvotes: 2

matt helliwell
matt helliwell

Reputation: 2678

You could do this if you combined the arguments into a single object and used a union to define the allowed types. Something like:

enum Endpoint {
    USERS = '/users/:userId',
    ORDERS = '/orders'
}

type Args = { endPoint: Endpoint.USERS; param: string } | { endpoint: Endpoint.ORDERS }

function callEndpoint(args: Args): {}

Upvotes: 4

Related Questions