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.