Search code examples
typescriptclass-transformer

class transformation of discriminated unions


I played around with routing-controllers and it's build-in class-transformer ability. I tried to build an interface where I can perform a search query based either on a location id or location coordinate. So I intended to use a discriminated union as a body parameter and could not get it to work. (See last console output what I mean by 'not working')

As an example:

interface LocationCoordinates {
    type: 'coordinate'
    longitude: number
    latitude: number
}

interface LocationId {
    type: 'id'
    id: number
}

class LocationRadius {
    data: LocationCoordinates | LocationId
    searchRadiusInKm: number
}

// raw input for LocationCoordinates
const rawLocationCoordinates = {
    data: {
        longitude: 22,
        latitude: 33
    },
    searchRadiusInKm: 30
}


// raw input for LocationId
const rawLocationId = {
    data: {
        id: 1
    },
    searchRadiusInKm: 30
}
// transfrom both raw inputs
const realLocationCoordinates = plainToClass(LocationRadius, rawLocationCoordinates);
const realLocationId = plainToClass(LocationRadius, rawLocationId);


console.log({
    coordinateType: realLocationCoordinates.data.type, // expect 'coordinate' but got 'undefinded'
    idType: realLocationId.data.type // expect 'id' but got 'undefinded'
});

Is there a way to achive this?


Solution

  • You can do this, but you will need some changes:

    1. LocationId and LocationCoordinates should be classes
    2. You should add a @Type decorator to the input property. This allows class-transformer to handle the deserialization based on a specific discriminator parameter
    class LocationRadius {
     @Type(() => Object, {
         keepDiscriminatorProperty: true,
         discriminator: {
             property: "type",
             subTypes: [
                 { value: LocationCoordinates, name: "coordinate" },
                 { value: LocationId, name: "id" }
             ]
         }
     })
     data: LocationCoordinates | LocationId
     searchRadiusInKm: number
    }
    
    1. You should add a type property to your input, in order to allow even TS to discriminate between the union:
     // raw input for LocationCoordinates
    const rawLocationCoordinates = {
       data: {
           type: "coordinate",
           longitude: 22,
           latitude: 33
       },
       searchRadiusInKm: 30
    }
    

    You can see the result in this StackBlitz project I set up