Type for an entity with optional fetchable relations

Viewed 41

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
1 Answers

Are you looking for something like this?:

type BusinessEntity = {
  id: string;
  fullName: string;
  email: string;
};

type CustomPaymentRequest<Fetched extends ('payer' | 'payee')[]> = {
  id: string;
  payer: 'payer' extends Fetched[number] ? BusinessEntity : string;
  payee: 'payee' extends Fetched[number] ? BusinessEntity : string;
  dueDate: string;
  amount: number;
};

let plain: CustomPaymentRequest<[]>;
let single: CustomPaymentRequest<['payer']>;
let double: CustomPaymentRequest<['payer', 'payee']>;

type O = CustomPaymentRequest<['payer']>['payee'] // string
type OO = CustomPaymentRequest<['payee']>['payee'] // BusinessEntity

Playground

Related