2

In our codebase we use a navigator and builder patterns pretty extensively to abstract away assembling hierarchical objects. At the heart of this is a Navigator class which we use to traverse different classes. I'm currently attempting to migrate this to typescript but am struggling to type it to leverage the power of typescript.

I think the core of my problem is that I can't use this as the default value for a generic on a class e.g. class Something<T = this>, or that I can't overload the class to somehow conditionally set the types of class properties. Can you provide any insights into how I might be able to type the Navigator (and builder classes) below?

// I guess what I'd like to do is
// class Navigator<BackT = this> but that's not allowed
class Navigator<BackT> {
  // It's this 'back' type I'll like to define more specifically
  // i.e. if back is passed to the constructor then it should be 'BackT'
  //      if back is not passed to the constructor or is undefined, it 
  //      should be 'this'
  back: BackT | this; 

  constructor(back?: BackT)  {
    this.back = back || this;
  }
}

class Builder1<BackT> extends Navigator<BackT> {
  builder1DoSomething() {
    // Do some work here
    return this;
  }
}

class Builder2<BackT> extends Navigator<BackT> {
  withBuilder1() {
    return new Builder1(this);

    // Also tried the following, but I got the same error:
    // return new Builder1<this>(this);
  }

  builder2DoSomething() {
    // Do some work here
    return this;
  }
}

// This is fine
new Builder1().builder1DoSomething().builder1DoSomething();

new Builder2()
  .withBuilder1()
  .builder1DoSomething()
  // I get an error here becuase my types are not specific enough to
  // let the complier know 'back' has taken me out of 'Builder1' and
  // back to 'Builder2'
  .back.builder2DoSomething();

playground link

2 Answers 2

3

You can use a conditional type on the back field to type it as this if no type argument was supplied to the class. We will use the void type as the default to signal the absence of a type argument:

class MyNavigator<BackT = void> {
  back: BackT extends void ? this : BackT; // Conditional type 

  constructor(back?: BackT)  {
    this.back = (back || this) as any;
  }
}

class Builder1<BackT = void> extends MyNavigator<BackT> {
  builder1DoSomething() {
    return this;
  }
}

class Builder2<BackT = void> extends MyNavigator<BackT> {
  withBuilder1() {
    return new Builder1(this);
  }
  builder2DoSomething() {
    return this;
  }
}

new Builder2()
  .withBuilder1()
  .builder1DoSomething()
  // ok now
  .back.builder2DoSomething();
Sign up to request clarification or add additional context in comments.

Comments

0

I think your best option is to create a special variant of every class which works like your option with no constructor parameter. So have a special SelfNavigator, SelfBuilder1 etc. extending their corresponding classes, and having no generic types themselves.

class MyNavigator<BackT> {
  back: BackT;

  constructor(back: BackT) {
    this.back = back;
  }
}

class SelfMyNavigator extends MyNavigator<SelfMyNavigator> {}

class Builder1<BackT> extends MyNavigator<BackT> {
  builder1DoSomething() {
    // Do some work here
    return this;
  }
}

class SelfBuilder1 extends Builder1<SelfBuilder1> {
  constructor() {
    super((null as unknown) as SelfBuilder1);
    this.back = this;
  }
}

class Builder2<BackT> extends MyNavigator<BackT> {
  withBuilder1() {
    return new Builder1(this);
  }
  builder2DoSomething() {
    // Do some work here
    return this;
  }
}

class SelfBuilder2 extends Builder2<SelfBuilder2> {
  constructor() {
    super((null as unknown) as SelfBuilder2);
    this.back = this;
  }
}

// This is fine
new SelfBuilder1().builder1DoSomething().builder1DoSomething();

new SelfBuilder2()
  .withBuilder1()
  .builder1DoSomething()
  .back.builder2DoSomething();

playground link

Note that you cannot call super(this) so the ugly casts are used. There are different ways to avoid this, to make the code nicer, e.g. convert everything to a system of interfaces and abstract classes so that the field back is not in the superclass, but rather a superinterface. Or make it a getter and return this if not set, although this will still need some casts.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.