1

I want to be able to define a class with a static property on it that contains an enum. I've got the JavaScript working here, I'm just trying to iron out the types. Simplified example below.

box.ts:

enum Color {
  RED,
  GREEN,
  BLUE,
}

export class Box {
  static Color = Color;

  private color: Color;

  constructor(color: Color) {
      this.color = color;
  }
}

main.ts:

import {Box} from './box';

function createBox(color: Box.Color): Box {
// ERROR: Box.Color isn't a type ^
  return new Box(color);
}

const box = createBox(Box.Color.RED);

As seen above, I want to be able to reference the Color type without importing it directly. I want to reference it as a member of Box, either Box.Color or Box['Color'] or similar. The JavaScript Box.Color.RED works fine as a value, but how do I refer to the type relative to Box? Is there something I can add to Box's definition to also export the Color type along with it?

I do not want to export enum Color and then import {Color}. I want to refer to it as a Box Color somehow and be able to just pass around the one Box type to contain the information.


I have found this solution, but it is far too verbose:

function createBox(color: (typeof Box.Color)[keyof typeof Box.Color]): Box {
  return new Box(color);
}

or, similarly, but it is too much of a hack:

enum Color {RED, GREEN, BLUE}

class Box {
  static Color = Color;
  // Defining a value that I don't use works along with `typeof`
  // to get the type, but seems like a poor hack and makes
  // for a bad API.
  static color: Color;
  constructor(color: Color) {}
}

function createBox(color: typeof Box.color): Box {
  return new Box(color);
}

Is there anyway to modify the Box class to something like the following or anything of the sort?

export class Box {
  static Color = Color;
  // I want the `Color` type to export as part of the `Box` type.
  static type Color: Color;

  private color: Color;

  constructor(color: Color) {
      this.color = color;
  }
}
Nathan Wall
  • 6,426
  • 2
  • 22
  • 23

1 Answers1

2

See Merging Namespaces with Classes in the TypeScript Handbook (I've inlined the full documentation section at the end of this answer so that if the link changes, the content will persist here). An example tailored to the data in your question:

TS Playground

export class Box {
  constructor(private color: Box.Color) {}
}

export namespace Box {
  export enum Color {
    RED,
    GREEN,
    BLUE,
  }
}

function createBox (color: Box.Color): Box {
  return new Box(color);
}

const box = createBox(Box.Color.RED);
console.log(box); // { color: 0 }


From the handbook:

Merging Namespaces with Classes

This gives the user a way of describing inner classes.

class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel {}
}

The visibility rules for merged members is the same as described in the Merging Namespaces section, so we must export the AlbumLabel class for the merged class to see it. The end result is a class managed inside of another class. You can also use namespaces to add more static members to an existing class.

In addition to the pattern of inner classes, you may also be familiar with the JavaScript practice of creating a function and then extending the function further by adding properties onto the function. TypeScript uses declaration merging to build up definitions like this in a type-safe way.

function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
  export let suffix = "";
  export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

Similarly, namespaces can be used to extend enums with static members:

enum Color {
  red = 1,
  green = 2,
  blue = 4,
}

namespace Color {
  export function mixColor(colorName: string) {
    if (colorName == "yellow") {
      return Color.red + Color.green;
    } else if (colorName == "white") {
      return Color.red + Color.green + Color.blue;
    } else if (colorName == "magenta") {
      return Color.red + Color.blue;
    } else if (colorName == "cyan") {
      return Color.green + Color.blue;
    }
  }
}
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • And thanks for adding the details about how to include static members in enums! That's actually something I'm doing now in a much more hacky way. I will transition to using namespaces. โ€“ Nathan Wall May 27 '22 at 00:22
  • 1
    @NathanWall I suggest avoiding enums unless it _really_ suits your case exactly. It's an artifact of early TypeScript, and (almost) all aspects of enums can be replicated using the `const myEnum = {/*...*/} as const;` syntax. (Of course, you'll have to create a separate "enum" _type_ from that data structure ([example](https://tsplay.dev/N7O7rN)), but it's better clarity IMO.) Just my 2ยข. โ€“ jsejcksn May 27 '22 at 00:27
  • I have a follow up question here regarding what happens when you subclass `Box`: https://stackoverflow.com/questions/72704806/subclass-a-class-with-an-enum-static-property-and-keep-type-information โ€“ Nathan Wall Jun 21 '22 at 17:22