Search code examples
javatypescriptfunctional-programmingabstract-classinner-classes

is it possible to implement a typescript abstract inner class?


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;
    }
  }

}


Solution

  • 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