1

I'm new to typescript and 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?

2 Answers 2

5

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

Sign up to request clarification or add additional context in comments.

2 Comments

Great answer and a good reminder that I need to stop approaching TS as I would javascript, theres usually a better way!
@Jamiec - :-) Thanks! But I may not have read the question correctly (or maybe I did, but just saying). Your approach may be what they want, if trailer is optional for pickups.
3

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.