2

How do I write an abstract inner class like this in typescript?

// java code
public abstract class StringMap {

 abstract static class Builder<T extends Builder<?, ?>, I> {
   protected final Map<String, String> map = new HashMap<>();

    protected Builder(Map<String, String> map) {
      this.map.putAll(emptyMapIfNull(map));
    }

   abstract T self();

    public abstract I build();

    public T put(String key, String value) {
      this.map.put(key, value);
      return self();
    }
 }

}

This is what I have so far but it will not build for multiple reasons. Can someone point me in the direction of how to convert the java version into typescript?

// typescript code
export abstract class StringMap {
 // it will not allow me to make this Builder assignment abstract 
 // which causes errors for the self and build function within it

 public static Builder =  class Builder<T extends Builder<any, any>, I> {
    constructor(map: Map<string, string>) {
     // implement putAll here
    }

    protected readonly map: Map<string, string> = new Map();

    abstract build(): I;

    public put(key: string, value: string): T {
      this.map.set(key, value);

      return this;
    }
  }

}

Sankofa
  • 600
  • 2
  • 7
  • 19
  • This has been helpful. Maybe I can use an interface instead https://stackoverflow.com/questions/13437922/why-nested-abstract-class-in-java – Sankofa Sep 09 '22 at 16:54
  • 1
    There is no support in TS for abstract static things, as mentioned in [ms/TS#34516](https://github.com/microsoft/TypeScript/issues/34516). Not sure why you need this, but you could work around it like [this](https://tsplay.dev/NBR9xN). Note that I dispensed with your explicit F-bounded polymorphism because TS has [polymorphic `this` types](https://www.typescriptlang.org/docs/handbook/2/classes.html#this-types); unless you're asking about F-bounded quantification, maybe you could remove that from your example? Otherwise there's too much going on in the question. – jcalz Sep 09 '22 at 16:58
  • Thank you for taking a stab at this. I am trying to implement an SDK for a project using functional programming. It has proved to have low defects using this design but I know typescript may not have all of the same capabilities. – Sankofa Sep 09 '22 at 17:03
  • I like your typescript playground I will see if this can work. tyvm!! – Sankofa Sep 09 '22 at 17:04
  • 1
    Okay I will write up an answer when I get a chance. In the mean time, please consider [edit]ing the question code to be a [mre] that doesn't include too many things you're not asking about. To be clear, TS doesn't need Java's `self()` and `class Foo>` recursive generics if the goal is just to refer to the implementing subclass with `T`; there is a type named `this` which works that way out of the box. So you can minimize your example to [this code here](https://tsplay.dev/mpLY7m). – jcalz Sep 09 '22 at 17:08
  • OK and yes I agree that ts doesnt need self but I do believe I need class Foo> recursive generics because this StringMap will be used extensively for other locator classes. So I probably got ahead of myself. I am using your first example now and it is a good start. – Sankofa Sep 09 '22 at 17:27

1 Answers1

1

Neither JavaScript nor TypeScript have true "inner" or "nested" classes (see Nested ES6 classes?) where you just declare a class inside another class.

// THIS IS NOT VALID, DON'T DO THIS
class Foo { 
    class Bar { } // nope
    static class Baz { } // nope
}

Instead you could give the outer class an instance or static member whose type is a class constructor. It's easy enough to initialize these with class expressions, to much the same effect:

class Foo {
    Bar = class Bar { } // okay
    static Baz = class Baz { } // okay
}

Unfortunately, you want your inner class to be abstract, and TypeScript does not support abstract class expressions; see microsoft/TypeScript#4578 for the declined feature request:

// THIS IS NOT VALID, DON'T DO THIS
class Foo {
    Bar = abstract class Bar { } // nope
    static Baz = abstract class Baz { } // nope
}

To work around that, one might write abstract class declarations in an appropriate scope and then assign them to relevant properties. This is helped by class property inference from initialization and static initialization blocks in classes:

class Foo {
    Bar; // type inferred from constructor initialization
    constructor() {
        abstract class Bar { } // declaration
        this.Bar = Bar; // initialization
    }

    static Baz; // type inferred from static initialization
    static {
        abstract class Baz { } // declaration
        this.Baz = Baz; // initialization
    }
}

In the above, Bar is an abstract instance-nested class, and Baz is an abstract static-nested class:

const foo = new Foo();
new foo.Bar(); // error, abstract
class MyBar extends foo.Bar { }
new MyBar(); // okay

new Foo.Baz(); // error, abstract
class MyBaz extends Foo.Baz { }
new MyBaz(); // okay

So, in your example, you could do something like

abstract class StringMap {
    public static Builder;
    static {
        abstract class Builder<I> {
            constructor(map: Map<string, string>) { }
            protected readonly map: Map<string, string> = new Map();
            abstract build(): I;
            public put(key: string, value: string): this {
                this.map.set(key, value);
                return this;
            }
        }
        this.Builder = Builder;
    }
}

That compiles fine as long as you aren't trying to generate declaration files via the --declaration compiler option, since it would require the compiler to provide declarations for undeclared types with protected things and... well, it's kind of a mess; see microsoft/TypeScript#35822. Workarounds include removing protected modifiers, or moving scopes around so the class with protected members has an exportable name. That's easy-ish for static-nested classes:

abstract class Builder<I> {
    constructor(map: Map<string, string>) { }
    protected readonly map: Map<string, string> = new Map();
    abstract build(): I;
    public put(key: string, value: string): this {
        this.map.set(key, value);
        return this;
    }
}

abstract class StringMap {
    public static Builder = Builder;
}

But the particular solution for how to deal with declaration files will depend on your use cases, so I'll stop there.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360