Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - how to do a conditional return type based on optional boolean parameters with a default values

I am converting some existing JS code into TS, and we've used a pattern I can't figure out how to express correctly with typescript :

function getVehicles({
    brandFields = false,
    ownerFields = false,
    maintenanceFields = false
} = {}) {
    // building and executing some SQL
}

Our repositories rely heavily on this kind of pattern, where we put the data that's costly to fetch behind a flag, and one function can have several of these flags.

Now, trying to type the different parts of the returned value is a bit of work, but it works out nicely :

type Vehicle = { id: dbId, manufactureDate: Date, color: string }
type VehicleBrand = { brandName: string, brandCountry: string }
type VehicleOwner = { owner: Person }
type VehicleMaintenance = { maintenance: { date: Date, place: string, operation: string } [} }

function getVehicles({
    brandFields = false,
    ownerFields = false,
    maintenanceFields = false
} = {}): (Vehicle & VehicleBrand & VehicleOwner & VehicleMaintenance) [] {
    // building and executing some SQL
}

But I want to make the return type more precise. This SO question suggest doing overloads, but it wouldn't be practical in this case due to the numbers of permutations.

So I think the only option left for me is to use generics and conditional types, with something along the lines of :

// With only one parameter for simplicity
function getVehicles<
    Brand extends boolean
>({
    brandFields: Brand = false
} = {}): (
    Vehicle &
    (Brand extends true ? VehicleBrand : {})
) [] {
    // building and executing some SQL
}

But I haven't found a way to make typescript happy while returning the narrowest type possible in all situations.

getVehicles()                         // should return Vehicle
getVehicles({ brandFields: false })   // should return Vehicle
getVehicles({ brandFields: true })    // should return Vehicle & VehicleBrand
getVehicles({ brandFields: boolean }) // should return Vehicle & (VehicleBrand | {})

The closest I've come is with this signature, but it is too lax :

function getVehicles<
    Brand extends boolean
>({
    brandFields: Brand | false = false // <-- union to avoid an error ...
} = {}): (
    Vehicle &
    (Brand extends true ? VehicleBrand : {})
) [] {
    // building and executing some SQL
}

getVehicles({ brandFields: true }) // but returns Vehicle & (VehicleBrand | {}) in this case

Is this even achievable with the current limitation of typescript ?

like image 528
VincentGuinaudeau Avatar asked Mar 21 '26 04:03

VincentGuinaudeau


1 Answers

You can achieve this with conditional types, like this:

type Vehicle<O extends OptionsFlags> = VehicleBase &
  (O extends { brandFields: true }
    ? VehicleBrand
    : O extends { brandFields: false | undefined }
    ? {}
    : VehicleBrand | {}) &
  (O extends { ownerFields: true }
    ? VehicleOwner
    : O extends { ownerFields: false | undefined }
    ? {}
    : VehicleOwner | {}) &
  (O extends { maintenanceFields: true }
    ? VehicleMaintenance
    : O extends { maintenanceFields: false | undefined }
    ? {}
    : VehicleMaintenance | {});

interface OptionsFlags {
  brandFields?: boolean;
  ownerFields?: boolean;
  maintenanceFields?: boolean;
}

interface VehicleBase {
  id: dbId;
  manufactureDate: Date;
  color: string;
}
interface VehicleBrand {
  brandName: string;
  brandCountry: string;
}
interface VehicleOwner {
  owner: Person;
}
interface VehicleMaintenance {
  maintenance: { date: Date; place: string; operation: string }[];
}

function getVehicles<O extends OptionsFlags>({
  brandFields = false,
  ownerFields = false,
  maintenanceFields = false,
}: O = {} as O): Vehicle<O>[] {
  // ...
}

getVehicles({ brandFields: true }) // return type is Vehicle<{ brandFields: true }>[]

However...

Depending on how you are using this, you may find it more helpful to define your Vehicle type like this, which allow each Vehicle subtype to resolve to a single interface that may have optional properties:

type Vehicle<O extends OptionsFlags> = VehicleBase &
  (O extends { brandFields: true }
    ? VehicleBrand
    : O extends { brandFields: false | undefined }
    ? {}
    : Partial<VehicleBrand>) &
  (O extends { ownerFields: true }
    ? VehicleOwner
    : O extends { ownerFields: false | undefined }
    ? {}
    : Partial<VehicleOwner>) &
  (O extends { maintenanceFields: true }
    ? VehicleMaintenance
    : O extends { maintenanceFields: false | undefined }
    ? {}
    : Partial<VehicleMaintenance>);

Vehicle<{ brandFields: boolean }> would then be equivalent to:

{
  id: dbId;
  manufactureDate: Date;
  color: string;
  brandName?: string;
  brandCountry?: string;
}
like image 195
Casey Rule Avatar answered Mar 23 '26 17:03

Casey Rule