[SOLVED] How can type-narrowing differ between assignment via ternary expressions and if-else statements?

Issue

It appears that the type checker is typing m in print() differently depending on if m was assigned via a ternary expression or via an if-else statement. What is the difference between the first line in the print() function and the commented code below that line?

interface Measurement {
  unit: string;
}

interface ComputedMeasurement extends Measurement {
  compute: (n: number) => number;
}

const measurements = {
  'Vitamin D': {
    unit: 'mcg',
  },
};

const computedMeasurements = {
  'Vitamin D (IU)': {
    unit: 'IU',
    compute: (n: number) => n / 0.025,
  },
};

type MeasurementKey = keyof typeof measurements;
type ComputedMeasurementKey = keyof typeof computedMeasurements;

const isMeasurementKey = (k: string): k is MeasurementKey =>
  k in measurements;

function print(key: MeasurementKey | ComputedMeasurementKey, amount: number): string {
  const m = isMeasurementKey(key) ? measurements[key] : computedMeasurements[key];
  // *** Replacing the above line with the below lines eliminates the below error. ***
  // let m: Measurement | ComputedMeasurement;
  // if (isMeasurementKey(key)) {
  //   m = measurements[key];
  // } else {
  //   m = computedMeasurements[key];
  // }

  if ('compute' in m) {
    // Error: Property 'compute' does not exist on type '{ unit: string; }'.
    return `${m.compute(amount)} ${m.unit}`;
  }
  return `${amount} ${m.unit}`;
}

Playground Link

Solution

Interesting, the difference is that the let declaration followed by the if-then-else assignments keeps the union type Measurement | ComputedMeasurement for m (which is expected behavior), but the conditional expression narrows the type to Measurement, which triggers the type error.

Turns out this is the intended behavior for conditional expressions where one alternative is a subtype of the other, as is explained in this closed TypeScript issue. One of the comments mentions that the subtype reduction can sometimes be a little unfortunate, and your example is one of these cases.

If you declare measurements and computedMeasurements as readonly objects with as const, you can use the conditional expression without any errors, as
the computed measurement will no longer be a subtype of the measurement:

const measurements = {
  'Vitamin D': {
    unit: 'mcg',
  },
} as const;

const computedMeasurements = {
  'Vitamin D (IU)': {
    unit: 'IU',
    compute: (n: number) => n / 0.025,
  },
} as const;

But you can also just use the if-then-else assignments.

TypeScript playground

Answered By – Oblosys

Answer Checked By – Senaida (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *