Home > Enterprise >  Make a function parameter required/optional based on another function parameter in Typescript
Make a function parameter required/optional based on another function parameter in Typescript

Time:07-12

Im new to typescript and Im trying to achieve type-checking for a function that accepts a third argument as optional. Then based on another function argument, that third argument is used or not used.

I made it optional but

  1. I get an error
  2. Even if I don't get the error I wonder if I can toggle the required/optional flag on the that parameter(trailer) based on another one (vehicleType).

Here is an example I prepared for this ocassion:

enum VehicleType {
  Car,
  Pickup
}

type Vehicle = {
  weight: number;
  length: number;
};

type Trailer = {
  weight: number;
  length: number;
};

function vehicle(
  vehicleType: VehicleType,
  vehicle: Vehicle,
  trailer?: Trailer
) {
  switch (vehicleType) {
    case VehicleType.Car:
      return `${vehicle.length} ${vehicle.weight}`;
    case VehicleType.Pickup:
      return `${vehicle.length   trailer.length} ${
        vehicle.weight   trailer?.weight
      }`;
  }
}

For this code I get the same error twice:

Object is possibly 'undefined'. For trailer. object

Is there a way to force the compiler to demand the trailer if the type is Pickup, and not to, when the type is Car?

Thank you

CodePudding user response:

I take it from the question that if vehicleType is Pickup, trailer is a required argument.

If so, the issue is that TypeScript doesn't know that; as far as it knows, vehicle(VehicleType.Pickup, someVehicle) (with no trailer) is perfectly valid.

You can tell it the specific combinations of parameters that are valid using function overloads; it doesn't completely solve the problem, but it gets us close:

function vehicle(vehicleType: VehicleType.Car, vehicle: Vehicle): string;
function vehicle(vehicleType: VehicleType.Pickup, vehicle: Vehicle, trailer: Trailer): string;
function vehicle(vehicleType: VehicleType, vehicle: Vehicle, trailer?: Trailer): string {
    switch (vehicleType) {
        case VehicleType.Car:
            return `${vehicle.length} ${vehicle.weight}`;
        case VehicleType.Pickup:
            assertNotNullish(trailer);
            return `${vehicle.length   trailer.length} ${vehicle.weight   trailer.weight}`;
    }
}

Only the first two of those are public signatures; the third one, with the implementation, is just the implementation signature and isn't seen by any other code.

Now the problem is that in the implemenation code, it's still complaining that trailer may be undefined, even though you know (from the overload signatures) that it won't be. There are two ways to solve that:

  • The not-nullish assertion operator, which is a postfix !
  • An explicit assertion

Here's the non-nullish version, note the ! before . when using trailer:

function vehicle(vehicleType: VehicleType.Car, vehicle: Vehicle): string;
function vehicle(vehicleType: VehicleType.Pickup, vehicle: Vehicle, trailer: Trailer): string;
function vehicle(vehicleType: VehicleType, vehicle: Vehicle, trailer?: Trailer): string {
    switch (vehicleType) {
        case VehicleType.Car:
            return `${vehicle.length} ${vehicle.weight}`;
        case VehicleType.Pickup:
            return `${vehicle.length   trailer!.length} ${vehicle.weight   trailer!.weight}`;
    }
}

Playground link

That assures TypeScript that we know trailer won't be undefined. (If we're wrong, we'll get an error at runtime along the lines of "Cannot read property 'lenght' of null").

That works, but many people don't like that kind of subtle assertion in the code (including me). Another option is to have a utility function lying around that you can use to explicitly document your assertion something won't be nullish:

function assertNotNullish<T>(value: T | null | undefined): asserts value is T {
    if (value ?? null === null) {
        throw new Error(`Got null/undefined value where non-null/non-undefined value expected`);
    }
}

That function tells TypeScript that if it completes without throwing an exception, the value we passed to it isn't null or undefined. Then we use it like this:

function vehicle(vehicleType: VehicleType.Car, vehicle: Vehicle): string;
function vehicle(vehicleType: VehicleType.Pickup, vehicle: Vehicle, trailer: Trailer): string;
function vehicle(vehicleType: VehicleType, vehicle: Vehicle, trailer?: Trailer): string {
    switch (vehicleType) {
        case VehicleType.Car:
            return `${vehicle.length} ${vehicle.weight}`;
        case VehicleType.Pickup:
            assertNotNullish(trailer);
            return `${vehicle.length   trailer.length} ${vehicle.weight   trailer.weight}`;
    }
}

Playground link

CodePudding user response:

I would suggest that instead of trying to handle making the third argument conditionally nullable, you just handle that internally. Either by throwing an exception, or combining what you have with the Nullish Coalescing operator ??

function vehicle(
  vehicleType: VehicleType,
  vehicle: Vehicle,
  trailer?: Trailer
) {
  switch (vehicleType) {
    case VehicleType.Car:
      return `${vehicle.length} ${vehicle.weight}`;
    case VehicleType.Pickup:
      return `${vehicle.length   (trailer?.length ?? 0)} ${
        vehicle.weight   (trailer?.weight ?? 0)
      }`;
  }
}

This then works in all cases

console.log(vehicle(VehicleType.Car, {weight:100, length:20}))
console.log(vehicle(VehicleType.Pickup, {weight:100, length:20}, {weight:10, length:5}))
console.log(vehicle(VehicleType.Pickup, {weight:100, length:20}))

Playground link

If you prefer the exception route, the act of checking for null means you no longer get possible null warnings from TS

function vehicle(
  vehicleType: VehicleType,
  vehicle: Vehicle,
  trailer?: Trailer
) {
  switch (vehicleType) {
    case VehicleType.Car:
      return `${vehicle.length} ${vehicle.weight}`;
    case VehicleType.Pickup:
      if(!trailer){
        throw "Trailer must be specified when VehicleType is Pickup"
      }
      return `${vehicle.length   trailer.length} ${
        vehicle.weight   trailer.weight
      }`;
  }
}

Playground link

  • Related