2

I have a notion of a Step, which requires value of type A as an input and gives out a value of type B.

class Step<A, B> {
  constructor(private readonly f: (a: A) => B) { }

  public run(a: A): B {
    return this.f(a);
  }
}

Now I would like to compose two steps, so I end up with something like this inside class Step:

  public andThen<C, D>(nextStep: Step<C, D>): Step<A, D> {
    return new Step<A, D>((state: A) => {
      const b: B = this.f(state);
      return nextStep.run(b);  // <---- compile error, B and C have no relation defined
    });
  }

What I would like to achieve is to somehow tell the type system that we can pass type B to a function which expects type C (structural typing should check that all fields in C are present in B), so that the line return nextStep.run(b) works fine.

Example:

const stepA: Step<{}, {a: number, b: string}> = new Step((input: {}) => ({ a: 5, b: "five" }));
const stepB: Step<{a: number}, {c: number}> = new Step((input: {a: number}) => ({c: input.a + 5}));

const steps = stepA.andThen(stepB)

As you can see stepB requires as an input {a: number}, so it can be fed an output from stepA which is {a: number, b: string}. But I cannot figure out how to define the relation in andThen. Any thoughts how this can be achieved?

ksaveljev
  • 550
  • 2
  • 16

1 Answers1

0

type SubType<T, WiderT> = T extends WiderT ? WiderT : never;

class Step<A, B> {
  constructor(private readonly f: (a: A) => B) { }

  public run(a: A): B {
    return this.f(a);
  }

  public andThen<C, D>(nextStep: Step<SubType<B, C> | B, D>): Step<A, D> {
    return new Step<A, D>((state: A) => {
      const b = this.f(state);
      return nextStep.run(b);
    });
  }
}

const stepAB: Step<{}, {a: number, b: string}> = 
new Step((input) => ({ a: 5, b: "five" }));

const add5 = ({ a }: { a: number }) => ({ c: a + 5 });
const stepCD: Step<{ a: number }, { c: number }> = new Step(add5);


const stepAD = stepAB.andThen(stepCD);

The core point is SubType type:

type SubType<T, WiderT> = T extends WiderT ? WiderT : never;

It says that if the type extends given type then its ok, but its not then never. Pay attention how it is used:

SubType<B, C> | B

So we are saying that if type B extends C, so B is more specific type then C then we allow on that, if not we don't by never. Additionally we allow on B itself (without union there was still compilation error)

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
  • Maciej, that is exactly what I started from. But now as you can see `stepBC` doesn't need property `b: string`, so as a separate entity it shouldn't require it as an input. `stepBC` becomes `Step<{a: number}, {c: number}>` and it doesn't work then. This is the question how do we tell that input to `stepBC` is structurally ok when we give it output of `stepAB` – ksaveljev Dec 17 '19 at 10:56
  • But what is a problem? You dont need to use `b`. you can work with `a` only. As you see `(input) => ({c: input.a + 5}` does not use `b`. Why do you need to remove it? – Maciej Sikora Dec 17 '19 at 11:03
  • function which work on wider type {a: number} can work with {a:number, b:string} also – Maciej Sikora Dec 17 '19 at 11:04
  • Check the answer now please – Maciej Sikora Dec 17 '19 at 11:22
  • yes, it seems to be doing exactly what i want it to do - thank you very much for your help, appreciate it – ksaveljev Dec 17 '19 at 11:34