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}`;
}
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.
Answered By – Oblosys
Answer Checked By – Senaida (BugsFixing Volunteer)