Raphael Castro
Raphael Castro

Reputation: 1148

How do I assert the type of a union type if a condition is met in Typescript?

I have an object that can either look like this:

{ageTop:42}

or like this:

{locations: [{name:Myrtle Beach}]}

These objects are passed as a parameter to a function. (I'm trying to make sure the function only gets the types that it can use, there may be one of these, there may be two of these there may be none of these)

the solution I have come up with is as follows:

I have defined the interface like so:

interface locationsType {
 locations: Array<{name: string}>
}

interface ageTopType {
ageTop: number
}

type keyType = 'ageTop' | 'locations'

I am enforcing the fact that if locations is selected we use the locationsType interface through convention in my code, I believe this will lead to errors later on when someone (or myself) breaks the convention. How can I enforce this through typing? Basically I'd like to create some simple logic as:

 interface traitType {
   key: keyType
   value: this.key === 'locations' ? locationsType : ageTopType;
}

class Developer {
  locations: []
  ageTop;
  constructor(ageTop,locations) {
  this.ageTop = ageTop
  locations.forEach(_val => this.locations.push(_val))
  }
  loadTrait(trait:TraitType) {
    if(trait.key === location) {
      this.trait.value.forEach(_val => this.locations.push(_val)
      return
    }
  this[trait.key] = trait.value;
 }
}

The above has been tried and does not work lol. Any insights?

Upvotes: 3

Views: 5008

Answers (2)

satanTime
satanTime

Reputation: 13539

you can try a union of types

interface locationsType {
    locations: Array<{name: string}>;
}

interface ageTopType {
    ageTop: number;
}

type keyType = 'ageTop' | 'locations';

enum traitTypes {
    location = 'locations',
    ageTop = 'ageTop',
}

type traitType = {
    key: traitTypes.location;
    value: Array<locationsType>;
} | {
    key: traitTypes.ageTop;
    value: ageTopType;
};

class Developer {
    public locations: Array<locationsType> = [];
    public ageTop: ageTopType;

    constructor(ageTop: ageTopType, locations: Array<locationsType>) {
        this.ageTop = ageTop;
        this.locations.push(...locations);
    }

    public loadTrait(trait: traitType): void {
        if (trait.key === traitTypes.location) {
            this[trait.key].push(...trait.value);
        } else if (trait.key === traitTypes.ageTop) {
            this[trait.key] = trait.value;
        }
    }
}

Upvotes: 1

jcalz
jcalz

Reputation: 328097

The conventional way to do this is via a discriminated union, where the members of the union have a common disciminant property that you test to narrow a value of the union type to one of the members. So given

interface LocationsType {
    locations: Array<{ name: string }>
}

interface AgeTopType {
    ageTop: number
}

We can define TraitType to be a discriminated union like this:

type TraitType = 
  { key: 'ageTop', val: AgeTopType } | 
  { key: 'locations', val: LocationsType };

And then you can see how the compiler automatically uses control flow analysis to refine the type of a TraitType when the discriminant is tested:

function foo(t: TraitType) {
    if (t.key === "ageTop") {
        t.val.ageTop.toFixed();
    } else {
        t.val.locations.map(x => x.name.toUpperCase());
    }
}

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 5

Related Questions