Home > Software design >  is it possible to implement a typescript abstract inner class?
is it possible to implement a typescript abstract inner class?

Time:09-11

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

}

CodePudding user response:

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

  • Related