Hypocritic
Hypocritic

Reputation: 41

Constrain function arguments to the same key

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

Answers (2)

Hypocritic
Hypocritic

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

Thomas Clark
Thomas Clark

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

Related Questions