bugs
bugs

Reputation: 15313

How to use types to enforce specific pairs of parameters?

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?

Upvotes: 2

Views: 646

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249466

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

Upvotes: 2

Related Questions