I have a REST API that can optionally fetch relations, if I ask it to do so: for example, if I call GET /payment-request I receive this payload (simplified for the sake of clarity):
[
{
"id": "01FBPR3K0RYBCRGCBWEC2AK30P",
"payer": "01FBRKNQRP74VBWW67N4BTH40B",
"payee": "01FB460C0FC6BSTK835VF5735J",
"dueDate": "2021-07-31",
"amount": 99988
}
]
But if I call GET /payment-request?fetch=payee I get
[
{
"id": "01FBPR3K0RYBCRGCBWEC2AK30P",
"payer": "01FBRKNQRP74VBWW67N4BTH40B",
"payee": {
"id": "01FB460C0FC6BSTK835VF5735J",
"fullName": "John Doe",
"email": "john@test.com"
},
"dueDate": "2021-07-31",
"amount": 99988
}
]
I can also fetch multiple relations at the same time, like GET /payment-request?fetch=payer,payee. Both payer and payee are not nullable, but I have also other entities that have a more lax constraints. Basically every relation could be nullable, and could point to a different type.
At the moment the types that I'm using on the frontend are pretty simple, but It feels quite clumsy and I need to do a good number of type checks in my code:
type BusinessEntity = {
id: string;
fullName: string;
email: string;
};
type PaymentRequest = {
id: string;
payer: string | BusinessEntity;
payee: string | BusinessEntity;
dueDate: string;
amount: number;
};
Today I've tried to put up some kind of generic to describe optional fetches, but it works only with a single fetch required:
type PaymentRequest<Fetched extends ('payer' | 'payee')[]> = {
id: string;
payer: Fetched extends ['payer'] ? BusinessEntity : string;
payee: Fetched extends ['payee'] ? BusinessEntity : string;
dueDate: string;
amount: number;
};
In this way I can do something, but not everything:
let plain : PaymentRequest<[]>; // OK: both payed and payee are string
let single : PaymentRequest<['payer']>; // OK: payer is a BusinessEntity, payee is a string
let double : PaymentRequest<['payer', 'payee']>; // WRONG: both payer and payee are string again
I have no clue about how to check if an "array generic type" contains a single value. Anyone has a piece of advice for me?
Final Solution
I've took captain-yossarian's solution and I've worked on a little further to simplify it:
type PaymentRequest<Fetched extends 'payer'|'payee'|''=''> = {
id: string;
payer: 'payer' extends Fetched ? BusinessEntity : string;
payee: 'payee' extends Fetched ? BusinessEntity : string;
dueDate: string;
amount: number;
};
Now the syntax is more concise, and I can just ignore the generic when I'm dealing with the standard output:
let plain : PaymentRequest; // OK: both payed and payee are string
let single : PaymentRequest<'payer'>; // OK: payer is a BusinessEntity, payee is a string
let double : PaymentRequest<'payer'|'payee'>; // OK: both payer and payee are BusinessEntity