Reputation: 41
I'm quite new to typescript and am trying to write some code that takes a number of different shapes of data from different sources and passes them to classes that will aggregate them (one aggregator class per data shape).
To do this, I believe I need a function (called dispatch
below) that will take a generic type of data and an identifier for the aggregator class of that data but I'm struggling with constraining the parameters enough for typescript to be happy.
This is what I have so far:
enum SourceIdentifier {
A = 'a',
B = 'b'
}
interface DataA {
prop1: number,
prop2: number[],
}
interface DataB {
prop3: number,
prop4: number,
}
class BaseAggregator<Data> {
public add(data: Data) {
// Store the data to be aggregated.
}
}
class AggregatorA extends BaseAggregator<DataA> {}
class AggregatorB extends BaseAggregator<DataB> {}
const managers = {
[SourceIdentifier.A]: new AggregatorA(),
[SourceIdentifier.B]: new AggregatorB()
}
// How to correctly constrain this function?
const dispatch = (data, source: SourceIdentifier) => {
managers[source].add(data);
};
I've tried using generics but am getting a typescript error:
const dispatch = <Data, Manager extends BaseAggregator<Data>>(data: Data, source: SourceIdentifier) => {
/*
* Type 'AggregatorA | AggregatorB' is not assignable to type 'Manager'.
* 'Manager' could be instantiated with an arbitrary type which could be unrelated to 'AggregatorA | AggregatorB'
*/
const manager: Manager = managers[source];
manager.add(data);
};
Is it actually possible to constrain a function like this or am I doomed due to the types not existing at runtime? Thanks, any help is appreciated.
Upvotes: 0
Views: 44
Reputation: 41
I've managed to acheive what I wanted by using discriminators like so:
enum SourceIdentifier {
A = 'a',
B = 'b'
}
interface DiscriminatedData {
discriminator: SourceIdentifier;
}
interface DataA extends DiscriminatedData {
discriminator: SourceIdentifier.A,
prop1: number,
prop2: number[],
}
interface DataB extends DiscriminatedData {
discriminator: SourceIdentifier.B,
prop3: number,
prop4: number,
}
type DataMessage = DataA | DataB;
class BaseAggregator<Data> {
public add(data: Data) {
// Store the data to be aggregated.
}
}
class AggregatorA extends BaseAggregator<DataA> {}
class AggregatorB extends BaseAggregator<DataB> {}
const managers = {
[SourceIdentifier.A]: new AggregatorA(),
[SourceIdentifier.B]: new AggregatorB()
}
const dispatch = (data: DataMessage) => {
const manager: BaseAggregator<unknown> = managers[data.discriminator];
manager.add(data);
};
// Example
dispatch({discriminator: SourceIdentifier.A, prop1: 2, prop2: [3, 3]})
Upvotes: 0
Reputation: 31
You were on the right track with your generics on the dispatch function. If you need to constrain a generic to be one of a particular set of types you can do so using the union operator (|
).
By writing something like this in your generics <Data extends DataA | DataB, ...>
You are constraining the generic type Data
to be either DataA
, DataB
or the union of the two. You can also do this for the generic in your BaseAggregator
class.
Upvotes: 1