Reputation: 15313
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
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