Narrow enum type to a single member

Viewed 36
enum Unit {
    Kilometer,
    Meter,
}

interface Measurement {
    distanceUnit: Unit
    distance: number
}

interface MetersMeasurement {
    distanceUnit: Unit.Meter
}

function someCalculation(item: MetersMeasurement){
    return;
}

function main(){
    const item : Measurement = {
        distance: 100,
        distanceUnit: Unit.Meter
    }

    if(item.distanceUnit !== Unit.Meter) return;
    
    someCalculation(item)
                 // ^^^^ Types of property 'distanceUnit' are incompatible.     
                 //      Type 'Unit' is not assignable to type 'Unit.Meter'.
}

Playground

  1. If MetersMeasurement and Measurement had the same fields, we could use a type predicate to narrow it down. But my problem requires that MetersMeasurement will only have a subset of the fields of Measurement. Therefore a type predicate wouldn't work [playground].

  2. We could also change the type of distanceUnit in the interface MetersMeasurement to Unit. And then move the if check for meters inside someCalculation function. But let's also say I don't want to do that. I don't want to give the function this responsibility.

  3. We could create a new object of type MetersMeasurement based on the original object and then pass that to someCalculation() instead. But this feels like a workaround (and extra code) because we couldn't narrow down the type [playground].

Is there a way to properly narrow down the type without breaking the rules? (no any or casting)

2 Answers

You can use a type guard to check whether or not the item is MetersMeasurement or Measurement

enum Unit {
    Kilometer,
    Meter,
}

interface Measurement {
    distanceUnit: Unit
    distance: number
}

interface MetersMeasurement {
    distanceUnit: Unit.Meter
}

function someCalculation(item: MetersMeasurement){
    return;
}

function isMeasurement(obj: Measurement): obj is Measurement {
  return (obj as Measurement).distance !== undefined;
}

function main(){
    const item = {
        distance: 100,
        distanceUnit: Unit.Meter
    }

    if(isMeasurement(item)) return;
    
    someCalculation(item)
}

Try this:

enum Unit {
    Kilometer,
    Meter,
}

type Distance = {
    distance: number;
}

type MetersMeasurement = {
    distanceUnit: Unit.Meter;
} & Distance

type KilometersMeasurement = {
    distanceUnit: Unit.Kilometer;
} & Distance

type Measurement = MetersMeasurement | KilometersMeasurement;


/*
 * return km
 */
function meterCalculation(item: MetersMeasurement) {
    return item.distance * .001;
}

function main(){
    const item : Measurement = {
        distance: 100,
        distanceUnit: Unit.Meter
    }

    if (item.distanceUnit == Unit.Meter) {
        meterCalculation(item);
    }
}

Playground

Related