2

I have a service responsible for generating different types of charts.

The space of charts that can be generated has two dimensions, chartType and dataType, which are both finite sets of values, something like this:

enum ChartType {
  ChartTypeA,
  ChartTypeB,
  ChartTypeC
}

enum DataType {
  DataTypeA,
  DataTypeB,
  DataTypeC
}

The service exposes a single public method, generateChart(chartType: ChartType , dataType: DataType), which then calls the relevant private method depending on what chartType is passed in.

The exact implementation of the private method depends on the other parameter, dataType.

So far so good.

My problem is the following, some combinations of (ChartType, DataType) are not possible (I.e. I can't generate a chart with ChartTypeA and DataTypeC), which makes me question my current implementation.

What is a better way to organise my data so that the compiler can enforce that only possible pairs of parameters are passed to the function?

bugs
  • 14,631
  • 5
  • 48
  • 52

1 Answers1

2

You can use overloads for each possible valid combination:

enum ChartType {
    ChartTypeA,
    ChartTypeB,
    ChartTypeC
}

enum DataType {
    DataTypeA,
    DataTypeB,
    DataTypeC
}

function generateChart(chartType: ChartType.ChartTypeA, dataType: DataType.DataTypeA)
function generateChart(chartType: ChartType.ChartTypeC, dataType: DataType.DataTypeC)
function generateChart(chartType: ChartType.ChartTypeB, dataType: DataType.DataTypeB)
function generateChart(chartType: ChartType, dataType: DataType) { // Implementation signature

}

generateChart(ChartType.ChartTypeA, DataType.DataTypeA)
generateChart(ChartType.ChartTypeA, DataType.DataTypeC) // Error

Or we can use a mapping type to cut down the ceremony a bit:

interface EnuMap  {
    [ChartType.ChartTypeA]: DataType.DataTypeA,
    [ChartType.ChartTypeB]: DataType.DataTypeB,
    [ChartType.ChartTypeC]: DataType.DataTypeC,
}

function generateChart<T extends ChartType>(chartType: T, dataType: EnuMap[T])
function generateChart(chartType: ChartType, dataType: DataType) { // Implementation signature

}

generateChart(ChartType.ChartTypeA, DataType.DataTypeA)
generateChart(ChartType.ChartTypeA, DataType.DataTypeC) // Error

Note If we use an interface for the mapping type, the interface can be extended as needed by a plugin for example if it ads support for a new combination of types.

Edit

If most combinations are possible and only a few should be excluded we could use a different approach. First create a type that contains all possible combinations of parameters and the use Exclude to take out the imposible combinations:

function generateChart<T extends Excluded>(...a: T)
function generateChart(chartType: ChartType, dataType: DataType) { // Implementation signature

}
type AllCombinations = {
    [C in ChartType]: {
        [D in DataType]: [C, D]
    }
}[ChartType][DataType]
// Exclude unwanted combinations
type Excluded = Exclude<AllCombinations, [ChartType.ChartTypeA, DataType.DataTypeC]>; 

generateChart(ChartType.ChartTypeA, DataType.DataTypeA)
generateChart(ChartType.ChartTypeB, DataType.DataTypeA)
generateChart(ChartType.ChartTypeA, DataType.DataTypeC) // Error

We lose a bit of expresivness with this approach in parameter names and the overloads the compiler suggests (just a code completion thing, it works as expected)

A solution that plays nicer with intelisense and keeps parameter names could be constructed using UnionToIntersection from here. We first create a union of all possible signatures and then we use UnionToIntersection to create a function with all overloads.

type AllCombinations = {
    [C in ChartType]: {
        [D in DataType]: [C, D]
    }
}[ChartType][DataType]
type Excluded = Exclude<AllCombinations, [ChartType.ChartTypeA, DataType.DataTypeC]>;
type UnionToIntersection<U> = 
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type SignatureHelper<T> = T extends [infer C, infer D] ? (chartType: C, dataType: D) => void : never;
type GenerateChartType = UnionToIntersection<SignatureHelper<Excluded>>
const generateChart:GenerateChartType  = (chartType: ChartType, dataType: DataType) => { // Implementation signature

}
generateChart(ChartType.ChartTypeA, DataType.DataTypeA)
generateChart(ChartType.ChartTypeB, DataType.DataTypeA)
generateChart(ChartType.ChartTypeA, DataType.DataTypeC) // Error
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • Interesting, the first solution could certainly be an option. What if the space was much bigger, with just a few impossible combinations? That would force me to overload every possible pair. The second solution looks more like what I had in mind, give me a second to try and understand it. – bugs Oct 12 '18 at 16:44
  • 1
    @bugs The second option is the same, not much of a difference except syntax. Let me see if we can do a cartesian product between the two enums and then eliminate the extra ones .. – Titian Cernicova-Dragomir Oct 12 '18 at 16:45
  • 1
    @bugs Added more versions, the types get a bit complicated but the last oe works out nice in my opinion. Let me know if I can help with anything :) – Titian Cernicova-Dragomir Oct 12 '18 at 16:58
  • Great, just having a look at everything now – bugs Oct 12 '18 at 17:25