Reputation: 1388
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
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
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