3

Follow up from: Define enum as static property of class

In the following code we have a Box class with a Color enum property defined on it such that the type can be referenced with Box.Color.

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

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 }

My problem is now when I try to subclass Box the Color property carries over (in JavaScript) but the type information doesn't carry over in TypeScript.

class SmallBox extends Box { }

function createSmallBox (color: SmallBox.Color): SmallBox {
  //                            ^ Error
  return new SmallBox(color);
}

const box = createSmallBox(SmallBox.Color.RED);
//                         ^ Works
console.log(box);

This works in JavaScript because SmallBox inherits the static Color property from Box. However, it doesn't work in TypeScript because the type information is lost. Is there some way to accomplish the same thing while maintaining the type information? I'm looking for a generic way that will keep all type information from Box and apply it to SmallBox without having to create a new SmallBox namespace and list every type manually.


Edit: I have been able to come up with the following which requires an extra namespace to pass things around. I would love to get rid of the extra sub-namespace if possible though.

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

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

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


class SmallBox extends Box { }
namespace SmallBox {
  export import Subtypes = Box.Subtypes;
}

function createSmallBox (color: SmallBox.Subtypes.Color): SmallBox {
  //                            ^ Works
  return new SmallBox(color);
}

const box = createSmallBox(SmallBox.Subtypes.Color.RED);
//                         ^ Works
console.log(box);
Nathan Wall
  • 6,426
  • 2
  • 22
  • 23
  • To be fair, it's surprising that `SmallBox.Color.RED` works, – after all, there's no namespace called `SmallBox`. The fact that `Box` (namespace) and `Box` (class) are merged together, and `SmallBox` is derived from `Box` (class) shouldn't be relevant. – Parzh from Ukraine Jun 21 '22 at 17:30
  • So, my take is that it is either a bug, or a poor design choice (maybe inherited from other, more established languages) – Parzh from Ukraine Jun 21 '22 at 17:32
  • Is it conceptually consistent to use `createSmallBox(Box.Color.RED)` in your program? – Parzh from Ukraine Jun 21 '22 at 17:33
  • No, it won't work to reference `Box`. I'm working at a higher level of abstraction than what's presented in this example. Users don't have access to `Box`, it's an internal class. `SmallBox` is the example of what would be exposed. – Nathan Wall Jun 21 '22 at 17:46
  • 2
    You directed me here in [a comment to my answer to your other question](https://stackoverflow.com/questions/72399145/define-enum-as-static-property-of-class/72399242?noredirect=1#comment128423318_72399242). I previously suggested that you don't use enums, and that is still my advise. Here's an example (https://tsplay.dev/WJyAvm) which solves your current problem without them. If that meets your needs, I can write that up as an answer. If it doesn't, please update your question to include the additional clarifying criteria. – jsejcksn Jun 21 '22 at 18:25
  • My general feeling about enums is that unless you use them in very simple ways, they create more problems than they solve. Making an enum scoped to a class constructor that are automatically inherited by subclasses is... not very simple. – jcalz Jun 21 '22 at 18:33
  • @jsejcksn thanks. Having people refer to the types as `Values` is really sub-optimal. I'd prefer using `SmallBox.Values.Color` which looks possible (from my edit above) over that, but `SmallBox.Color` would be even better if possible. – Nathan Wall Jun 21 '22 at 19:06
  • @jsejcksn I don't care so much if they're defined as enum per se; that's just an easy way to illustrate the problem. I would be interested in reading more on why to avoid enums and use your form if you have a link. I think, if I understand correctly, the form you're presenting wouldn't work for me though because I have a need to use `"isolateModules": true` – Nathan Wall Jun 21 '22 at 19:08
  • @NathanWall What is "sub-optimal" about it? Is it just your personal preference? If it's the verbosity of the syntax: TypeScript is definitely a verbose language, (although the team has been making some strides toward improving that in recent updates). – jsejcksn Jun 21 '22 at 19:25
  • @NathanWall Re: reasons to avoid enums: this has already been covered on [other SO questions](https://stackoverflow.com/a/60041791/438273) and you can find lots of other discussion by using a search engine. Re: [`compilerOptions.isolatedModules`](https://www.typescriptlang.org/tsconfig#isolatedModules): I'm not sure how it's relevant to the code I shared... can you clarify? – jsejcksn Jun 21 '22 at 19:25
  • @jsejcksn I'm not sure that I've tried it, nor do I fully understand your code, but I assumed the `as const` meant TypeScript would rewrite `Color.RED` to `1` eveywhere, and I didn't think that would work across modules if `Color` is exported with `isolatedModules` because that prevents TypeScript from understanding imports on that level during compilation. But again, I'm not married to enums, but it does make my example more succinct just to use enum syntax. – Nathan Wall Jun 21 '22 at 19:45
  • Yes, `Values` is way too verbose and hard to use. I'm working on a library that others will be using and I want their interface to the library to be simply `SmallBox.Color` to access the type. It's much cleaner and easier on the users of my library if I can set things up that way. Of course, defining it as an object or enum doesn't matter for solving that problem. You can always give a type a name. I think I can figure out how to change the enums to something else. But I can't figure out a scalable way to define sub-types that work on a sub-class – Nathan Wall Jun 21 '22 at 19:46
  • @NathanWall Re: isolated modules and value inlining: I think you might be confusing [`const` enums](https://www.typescriptlang.org/docs/handbook/enums.html#const-enums) and [`const` assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions). `as const` is a _`const` assertion_. Re: syntax: Yes, exporting a type alias would be useful in that case (that you expect your consumers to need the type). I'll include that in my answer. – jsejcksn Jun 22 '22 at 00:06

1 Answers1

0

Rather than use enums, you can define an object on your base class with the desired enumeration of values using as const (a const assertion), then create a type alias to represent a union of those enumerated values (BoxColor in the example below). Using this approach, everything is simpler: no namespace merging, no enum code generation, etc.

TypeScript Playground

type Values<T> = T[keyof T];

export type BoxColor = Values<typeof Box.Color>; // 0 | 1 | 2

class Box {
  static readonly Color = {
    RED: 0,
    GREEN: 1,
    BLUE: 2,
  } as const;

  constructor(private color: BoxColor) {}
}

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

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

export class SmallBox extends Box {}

export function createSmallBox (color: BoxColor): SmallBox {
  return new SmallBox(color);
}

const smallBox = createSmallBox(SmallBox.Color.RED);
console.log(smallBox); // { color: 0 }

If your consumers need access to that type, they can just import it alongside the value exports:

import {type BoxColor, createSmallBox} from './the-module-above.mts';
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • Thanks for the time, but unfortunately this doesn't solve my problem. I know you can export multiple things, including types, and then import those things and use them. My problem is I want to import `SmallBox` only and get all the pieces with it: the `SmallBox` class (works), the `SmallBox` type (works), the `SmallBox.Color` object (works), and the `SmallBox.Color` type (doesn't work). Why there's a way to have a value and type overlap but no way to have a obj.prop value and obj.prop type overlap, with inheritance, I'm not sure. – Nathan Wall Jun 22 '22 at 02:56
  • Thanks for the info on const assertions too! – Nathan Wall Jun 22 '22 at 02:59
  • @NathanWall If those things are criteria, then why didn’t you include them in your question? – jsejcksn Jun 22 '22 at 03:28