Home > Software engineering >  How to require the `this` type to be present in the generic of a method parameter
How to require the `this` type to be present in the generic of a method parameter

Time:10-16

I'm trying to write a class in TypeScript that allows me to connect different types of elements in a sort of network. The allowed target type for each type of element should be constrainable, so I wrote this really simple generic class:

abstract class Element<Target = unknown> {
  targets: Target[] = [];

  into(...targets: Target[]): this {
    // implementation omitted for brevity
    return this;
  }
}

This gives me correct type checking for things like this:

class First extends Element<First | Second> { // Can connect to First or Second
  firstProp = "";
}
class Second extends Element<First | Second> { // Can connect to First or Second
  secondProp = "";
}
class Third extends Element<Third> { // Can only connect to itself
  thirdProp = "";
}

const first = new First();
const second = new Second();
const third = new Third();

first.into(first); // works, OK
first.into(second); // works, OK
first.into(third); // error, OK

So far, so good. Then, I wanted to add a convenience method to the class that allows me to specify connections the other way around by specifying a set of sources to connect from. However, I can't seem to come up with the correct signature to get this done, so I tried to add this method:

from(...sources: Element<this>): this {
  // omitted again
  return this;
}

// This should work now:
first.from(second); // But errors

But it doesn't work because the signature as it is requires the sources to be of type Element<First>, whereas second is of type Element<First | Second>, so TypeScript errors correctly because First does not extend First | Second, but the other way around. This is a problem, as it's possible to write a generic like X extends this but, of course, not a generic like this extends X.

So the type of sources should be an array of Elements that have the this of from's instance in their Target union, but I have no idea how to pull something like this off in TypeScript or if it is even possible.

I have tried many variations to the signature, trying combinations with conditional types and other good stuff. Still, nothing has worked so far, and I slowly start to believe I have tried so many wrong things that I'm completely on the wrong track by now and that I'm missing something completely obvious here, so any input is greatly appreciated.

CodePudding user response:

  from<Sources extends Element<unknown>[]>(...sources: {
    [K in keyof Sources]: this extends Sources[K]["targets"][number] ? Sources[K] : Error & { error: ["This class's targets do not include", Target] };
  }): this {
    // omitted again
    return this;
  }

Using a generic to infer the types of the targets, we can then, inside the type of the rest parameters, check each of the given elements' target. If it doesn't match with this, then we "change" the expected type to Error & { ... }, essentially giving the user a helpful error message.

Playground

  • Related