Typescript: Nested class declarations (rather than expressions) in source files

Created on 2 Mar 2019  ·  7Comments  ·  Source: microsoft/TypeScript

Search Terms

nested class declaration static namespace

Suggestion

TypeScript currently allows writing

class Foo {
  static Bar = class {}
}

where Foo.Bar is a constructor to an anonymous class. Unfortunately, there's not a good way to write the type of Foo.Bar, other than InstanceType<typeof Foo.Bar>. Trying to write simply Foo.Bar as a type fails because Foo is not a namespace. Alternatively, one can separately open the namespace and declare the type there as well namespace Foo { export type Bar = InstanceType<typeof Foo.Bar>; }, but this is repetitive and error-prone.

Allowing a declaration of the form

class Foo {
  static class Bar {}
}

could potentially get over this issue by automatically defining the type Bar in the Foo namespace.

This is currently invalid syntax both in TS and in JS. It would be reasonable to also propose this as an extension to the static member fields proposal to TC39, though they don't make as much of a distinction as TypeScript does between const Foo = class {} and class Foo {} (only the latter declares a type), so the extra syntax is less important there.

Use Cases

Nested classes are particularly useful for two things: (1) readable APIs, and (2) encapsulation.

  1. By bundling closely-related classes together, one can provide clearer APIs. For example, when using the builder pattern, a module with a single export Foo with nested class Foo.Builder provides an unambiguously clearer API than separately exporting Foo and FooBuilder at the top level. Extending the latter example to its logical conclusion, you end up with enough exports that users will tend to import * as foo instead of import {Foo}, and thus write foo.FooBuilder all over the place, smurfing everything up.
  2. Since a nested class has access to its enclosing class's privates, it allows a very clear way for closely-collaborating classes to share access without unnecessarily widening access to any other code. This is useful, e.g. for "view" data structures - a SortedSet class with private field elems could have a nested class SortedSet.Reversed that stores a reference to the SortedSet and reads its elems field directly, which could allow for a smaller public API for SortedSet since the reverse can read private data to perform symmetric functionality.

As it currently stands, (2) is doable if you don't care to use the nested class as a type name, while (1) is very cumbersome in the current language.

Examples

export class Foo {
  static class Builder {
    private bar: number;
    withBar(bar: number): Foo.Builder {
      this.bar = bar;
      return this;
    }
    build() {
      return new Foo(this.bar);
    }
  }
  // ...
}

This would essentially desugar to the same thing as

export class Foo {
  static Builder = class { /* ... */ };
  // ...
}
namespace Foo {
  export type Builder = InstanceType<typeof Foo.Builder>;
}

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Out of Scope Suggestion

Most helpful comment

This is allowed, but has [a minor problem]: Bar cannot access the Foo class's privates like it could were it nested within class Foo. There's also the organizational problem that this distinction is unique to TypeScript, so it's unlikely TC39 would be interested in this, since as far as JavaScript is concerned, static class Bar {} is no different from static Bar = class {}, but since TypeScript does make a distinction between the two forms (in general), it leaves us in a bit of a fix.

All 7 comments

This is already allowed with the TypeScript syntax

class Foo {

}
namespace Foo {
  export class Bar {

  }
}

Adding an additional syntax inside the class itself is a proposal that should be taken to TC39.

This is allowed, but has [a minor problem]: Bar cannot access the Foo class's privates like it could were it nested within class Foo. There's also the organizational problem that this distinction is unique to TypeScript, so it's unlikely TC39 would be interested in this, since as far as JavaScript is concerned, static class Bar {} is no different from static Bar = class {}, but since TypeScript does make a distinction between the two forms (in general), it leaves us in a bit of a fix.

@RyanCavanaugh is there any solution for this problem ?

Lobby es-discuss or TC39 about the importance of having this feature

Honestly that's a bit of a non-answer. The feature is already as supported as it can be as far as TC39 is concerned - the only reason it's necessary is due to differences TS imposes _outside_ of the EcmaScript standard (specifically, the fact that class expressions don't make types, and visibility - yes, we're getting #privates soon, but constructor visibility is also a thing, i.e. in the builder example).

specifically, the fact that class expressions don't make types

export class Foo {
  static Builder = class {
    private bar: number = 0;
    withBar(bar: number): Foo.Builder {
      this.bar = bar;
      return this;
    }
    build() {
      return new Foo(this.bar);
    }
  }
  // ...
}
export namespace Foo {
    export type Builder = InstanceType<typeof Foo.Builder>;
}

Hi @RyanCavanaugh Can you take a look on this option for this use-case

class ParentClass {
  private var1 = 1;
  static svar1 = 2;
  ClassMember = ((rootThis) => class ChildClass {
    access() {
      console.log(ParentClass.svar1, rootThis.var1);
    }
  })(this);
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Roam-Cooper picture Roam-Cooper  ·  3Comments

uber5001 picture uber5001  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

siddjain picture siddjain  ·  3Comments

fwanicka picture fwanicka  ·  3Comments