Open nomaed opened 7 years ago
Just played with it a little bit and it is currently possible to do this extension using an object for the extended type, so this should work fine:
enum BasicEvents {
Start = "Start",
Finish = "Finish"
};
// extend enum using "extends" keyword
const AdvEvents = {
...BasicEvents,
Pause: "Pause",
Resume: "Resume"
};
Note, you can get close with
enum E {}
enum BasicEvents {
Start = 'Start',
Finish = 'Finish'
}
enum AdvEvents {
Pause = 'Pause',
Resume = 'Resume'
}
function enumerate<T1 extends typeof E, T2 extends typeof E>(e1: T1, e2: T2) {
enum Events {
Restart = 'Restart'
}
return Events as typeof Events & T1 & T2;
}
const e = enumerate(BasicEvents, AdvEvents);
Another option, depending on your needs, is to use a union type:
const enum BasicEvents {
Start = 'Start',
Finish = 'Finish'
}
const enum AdvEvents {
Pause = 'Pause',
Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
let e: Events = AdvEvents.Pause;
Downside is you can't use Events.Pause
; you have to use AdvEvents.Pause
. If you're using const enums, this is probably ok. Otherwise, it might not be sufficient for your use case.
We need this feature for strongly typed Redux reducers. Please add it in TypeScript.
Another workaround is to not use enums, but use something that looks like an enum:
const BasicEvents = {
Start: 'Start' as 'Start',
Finish: 'Finish' as 'Finish'
};
type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];
const AdvEvents = {
...BasicEvents,
Pause: 'Pause' as 'Pause',
Resume: 'Resume' as 'Resume'
};
type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];
All workarounds are nice but I would like to see the enum inheritance support from typescript itself so that I can use exhaustive checks as simple as possible.
Just use class instead of enum.
I was just trying this out.
const BasicEvents = {
Start: 'Start' as 'Start',
Finish: 'Finish' as 'Finish'
};
type BasicEvents = (typeof BasicEvents)[keyof typeof BasicEvents];
const AdvEvents = {
...BasicEvents,
Pause: 'Pause' as 'Pause',
Resume: 'Resume' as 'Resume'
};
type AdvEvents = (typeof AdvEvents)[keyof typeof AdvEvents];
type sometype<T extends AdvEvents> =
T extends typeof AdvEvents.Start ? 'Some String' :
T extends typeof AdvEvents.Finish ? 'Some Other String' :
T extends typeof AdvEvents.Pause ? 'Abc' :
T extends typeof AdvEvents.Resume ? 'Xyz' : never;
type r = sometype<typeof AdvEvents.Finish>;
There has got to be a better way of doing this.
Why isn't this a feature already? No breaking changes, intuitive behavior, 80+ people who actively searched for and demand this feature – it seems like a no-brainer.
Even re-exporting enum from a different file in a namespace is really weird without extending enums (and it's impossible to re-export the enum in a way it's still enum and not object and type):
import { Foo as _Foo } from './Foo';
namespace Bar
{
enum Foo extends _Foo {} // nope, doesn't work
const Foo = _Foo;
type Foo = _Foo;
}
Bar.Foo // actually not an enum
+1 Currently using a workaround, but this should be a native enum feature.
I skimmed through this issue to see if anyone has posed the following question. (Seems not.)
From OP:
enum BasicEvents {
Start = "Start",
Finish = "Finish"
};
// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
Pause = "Pause",
Resume = "Resume"
};
Would people expect AdvEvents
to be assignable to BasicEvents
? (As is, for example, the case with extends
for classes.)
If yes, then how well does that mesh with the fact that enum types are meant to be final and not possible to extend?
@masak great point. The feature people want here is definitely not like normal extends
. BasicEvents
should be assignable to AdvEvents
, not the other way around. Normal extends
refines another type to be more specific, and in this case we want to broaden the other type to add more values, so any custom syntax for this should probably not use the extends
keyword, or at least not use the syntax enum A extends B {
.
On that note, I did like the suggestion of spread for this from OP.
// extend enum using spread
enum AdvEvents {
...BasicEvents,
Pause = "Pause",
Resume = "Resume"
};
Because spreading already carries the expectation of the original being shallow-cloned into an unconnected copy.
BasicEvents
should be assignable toAdvEvents
, not the other way around.
I can see how that could be true in all cases, but I'm not sure it should be true in all cases, if you see what I mean. Feels like it'd be domain-dependent and rely on the reason those enum values were copied over.
I thought about workarounds a little more, and working off of https://github.com/Microsoft/TypeScript/issues/17592#issuecomment-331491147 , you can do a little better by also defining Events
in the value space:
enum BasicEvents {
Start = 'Start',
Finish = 'Finish'
}
enum AdvEvents {
Pause = 'Pause',
Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};
let e: Events = Events.Pause;
From my testing, looks like Events.Start
is correctly interpreted as BasicEvents.Start
in the type system, so exhaustiveness checking and discriminated union refinement seem to work fine. The main thing missing is that you can't use Events.Pause
as a type literal; you need AdvEvents.Pause
. You can use typeof Events.Pause
and it resolves to AdvEvents.Pause
, though people on my team have been confused by that sort of pattern and I think in practice I'd encourage AdvEvents.Pause
when using it as a type.
(This is for the case when you want the enum types to be assignable between each other rather than isolated enums. From my experience, it's most common to want them to be assignable.)
Another suggestion (even though it does not solve the original problem), how about using string literals to create a type union instead?
type BEs = "Start" | "Finish";
type AEs = BEs | "Pause" | "Resume";
let example: AEs = "Finish"; // there is even autocompletion
So, the solution to our problems could be this?
const BasicEvents = {
Start: "Start",
Finish: "Finish"
};
// { Start: string, Finish: string };
const BasicEvents = {
Start: "Start",
Finish: "Finish"
} as const
// { Start: 'Start', Finish: 'Finish' };
Extending enums should be a core feature of TypeScript. Just sayin'
@wottpal Repeating my question from earlier:
If [enums can be extended], then how well does that mesh with the fact that enum types are meant to be final and not possible to extend?
Specifically, it seems to me that the totality check of a switch statement over an enum value depends on the non-extensibility of enums.
@masak What? No, it doesn't! Since extended enum is a wider type and cannot be assigned to the original enum, you always know all the values of every enum you use. Extending in this context means creating a new enum, not modifying the old one.
enum A { a; }
enum B extends A { b; }
declare var a: A;
switch(a) {
case A.a:
break;
default:
// a is never
}
declare var b: B;
switch(b) {
case A.a:
break;
default:
// b is B.b
}
@m93a Ah, so you mean that extends
here in effect has more of a copying semantics (of the enum values from A
into B
)? Then, yes, the switches come out OK.
However, there is some expectation in there that still seems broken to me. As a way to try and nail it down: with classes, extends
does not convey copying semantics — fields and methods do not get copied into the extending subclass; instead, they are just made available via the prototype chain. There is only ever one field or method, in the superclass.
Because of this, if class B extends A
, we are guaranteed that B
is assignable to A
, and so for example let a: A = new B();
would be perfectly fine.
But with enums and extends
, we wouldn't be able to do let a: A = B.b;
, because there is no such corresponding guarantee. Which is what feels odd to me; extends
here conveys a certain set of assumptions about what can be done, and they are not met with enums.
Then just calling it expands
or clones
? 🤷♂️
From a users perspective it just feels odd that something that basic is not straightforward to achieve.
If the reasonable semantics requires a whole new keyword (without much of prior art in other languages), why not instead re-use the spread syntax (...
) as suggested in OP and this comment?
+1
I hope this will be added to the default enumeration feature. :)
Does anyone know any elegant solutions??? 🧐
If the reasonable semantics requires a whole new keyword (without much of prior art in other languages), why not instead re-use the spread syntax (...) as suggested in OP and this comment?
Yes, after thinking about it a bit more I think this solution would be good.
After reading this whole issue thread, it seems that there is a broad agreement that re-using the spread operator solves the issue, and addresses all of the concerns people have raised about making the syntax confusing/unintuitive.
// extend enum using spread
enum AdvancedEvents {
...BasicEvents,
Pause = "Pause",
Resume = "Resume"
};
Does this issue really still need the "Awaiting More Feedback" label at this point, @RyanCavanaugh ?
The feature wanted +1
Do we have any news on this issue ? It feels really useful to have the spreading operator implemented for enums.
Especially for use cases that involve metaprogramming the ability to alias and extend enums is somewhere between a must-have and a nice-to-have. There's no way currently to take one enum and export
it in another name - unless you resort to one of the workarounds mentioned above.
@m93a Ah, so you mean that
extends
here in effect has more of a copying semantics (of the enum values fromA
intoB
)? Then, yes, the switches come out OK.However, there is some expectation in there that still seems broken to me. As a way to try and nail it down: with classes,
extends
does not convey copying semantics — fields and methods do not get copied into the extending subclass; instead, they are just made available via the prototype chain. There is only ever one field or method, in the superclass.Because of this, if
class B extends A
, we are guaranteed thatB
is assignable toA
, and so for examplelet a: A = new B();
would be perfectly fine.But with enums and
extends
, we wouldn't be able to dolet a: A = B.b;
, because there is no such corresponding guarantee. Which is what feels odd to me;extends
here conveys a certain set of assumptions about what can be done, and they are not met with enums.
@masak I think you're close to correct but you made one small assumption that is incorrect. B is assignable to A in the event of enum B extends A
as "assignable" means that all values provided by A are available in B. When you said that let a: A = B.b
you are assuming that values in B must be available in A, which is not the same as the values being assignable to A. let a: A = B.a
IS correct because B is assignable to A.
This is evident using classes like in the following example:
class A {
a() {}
}
class B extends A {
b() {}
}
let a: A = new B();
a.a(); // valid
a.b(); // invalid via type system since `a` is typed as `A`
Long story short, I believe extends IS the correct terminology as that is exactly what is being done. In the enum B extends A
example you can ALWAYS expect a B enum to contain all the possible values of the A enum, because B is a "subclass" (subenum? maybe there is a better word for this) of A and thus assignable to A.
So I don't think we need a new keyword, I think we should use extends AND I think this should be natively a part of TypeScript :D
@julian-sf I think I agree with every thing you wrote...
...but... :slightly_smiling_face:
as I problematized here, what about this situation?
// example from OP
enum BasicEvents {
Start = "Start",
Finish = "Finish"
};
// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
Pause = "Pause",
Resume = "Resume"
};
Given that Pause
is an instance of AdvEvents
and AdvEvents
extends BasicEvents
, would you also expect Pause
to be an instance of BasicEvents
? (Because that seems to follow from how the instance/inheritance relations usually interact.)
On the other hand, the core value proposition of enums (IMHO) is that they are closed/"final" (as in, non-extensible) so that something like a switch
statement can assume totality. (And so AdvEvents
being able to extend what it means to be a BasicEvent
violates some kind of Least Surprise for enums.)
I don't think that you can more than two of the following three properties:
extends
relation between two enum
declarationsb
is an instance of B
and B extends A
, then b
is an instance of A
@masak I understand and agree with the closed principal of enums (at runtime). But compile time extension would not violate the closed principle at runtime, as they would all be defined and constructed by the compiler.
The (reasonable) assumption that if b is an instance of B and B extends A, then b is an instance of A
I think this reasoning is kind of misleading as the instance/class dichotomy is not really assignable to enum. Enums are not classes and they do not have instances. I do think however, that they can be extensible, if done properly. Think of enums more like sets. In this example, B is a superset of A. Therefore it is reasonable to assume that any value in A is present in B, but that only SOME values of B will be present in A.
I understand where the concern comes from...though. And I'm not sure what to do about that. A good example of an issue with enum extension:
const enum A { a = 'a' }
const enum B extends A { b = 'b' }
const foo = (a: A) => console.log(a);
const bar = (b: B) => foo(b);
bar(B.a); // 'a'
bar(B.b); // uh-oh, b doesn't exist on A, so foo would get unexpected behavior
// HOWEVER, this would work just fine...
const baz = (a: A) => bar(a);
baz(A.a); // 'a'
baz(B.a); // 'a'
baz(B.b); // compiler error as expected...
In this case, enums behave quite differently than classes. If these were classes, you would expect to be able to cast B to A quite easily, but that clearly won't work here. I don't necessarily think this is BAD, I think it should just be accounted for. IE, you can't scope an enum type upwards in its inheritance tree like a class. This could be accounted for with a compiler error along the lines of "cannot assign superset enum B to A, as not all values of B are present in A".
@julian-sf
I think this reasoning is kind of misleading as the instance/class dichotomy is not really assignable to enum. Enums are not classes and they do not have instances.
You're absolutely right, on the face of it.
Thinking about this, I realize that I'm colored a little bit by Java's take on enums. In Java, enum values are literally instances of their enum type. Implementation-wise, an enum is a class extending the Enum class. (You're not allowed to do it manually, you have to go through the enum
keyword, but that's what happens under the hood.) The nice thing about this is that enums get all the conveniences classes do: they can have fields, constructors, methods... In this appproach, enum members are instances. (The JLS says as much.)
Note that I'm not proposing any changes to TypeScript enum semantics. In particular, I'm not saying TypeScript should change to using Java's model for enums. I am saying that it's instructive/insightful to overlay a class/instance "understanding" on top of enums/enum members. Not "an enum is a class" or "an enum member is an instance"... but there are similarities that carry over.
What similarities? First and foremost, type membership.
enum Foo { A, B, C }
enum Bar { X, Y, Z }
let foo: Foo = Foo.C;
foo = Bar.Z;
The last line doesn't typecheck, because Bar.Z
is not a Foo
. Again, this is not actually classes and instances, but it can be understood using the same model, as if Foo
and Bar
were classes and the six members were their respective instances.
(We'll ignore for the purposes of this argument the fact that let foo: Foo = 2;
is also semantically legal, and that in general, number
values are assignable to variables of enum type.)
Enums have the additional property that they are closed — sorry, I don't know a better term for this — once you define them, you cannot extend them. Specifically, the members listed inside of the enum declaration are the only things that typematch against the enum type. ("Closed" as in "closed-world hypothesis".) This is a great property because you can verify with total certainty that all the cases in a switch
statement on your enum have been covered.
With extends
on enums, this property goes out the window.
You write,
I understand and agree with the closed principal of enums (at runtime). But compile time extension would not violate the closed principle at runtime, as they would all be defined and constructed by the compiler.
I don't think that's true, because it assumes that any code that extends your enum is in your project. But a third-party module can extend your enum, and suddenly there are new enum members that are also assignable to your enum, outside of the control of code that you compile. Essentially, enums would no longer be closed, not even at compile-time.
I still feel I'm somewhat clumsy in expressing exactly what I mean, but I believe it's important: extends
on enum
would break one of the most precious features of enums, the fact that they're closed. Please count how many languages absolutely forbid extending/subclassing an enum, for this very reason.
I thought about workarounds a little more, and working off of #17592 (comment) , you can do a little better by also defining
Events
in the value space:enum BasicEvents { Start = 'Start', Finish = 'Finish' } enum AdvEvents { Pause = 'Pause', Resume = 'Resume' } type Events = BasicEvents | AdvEvents; const Events = {...BasicEvents, ...AdvEvents}; let e: Events = Events.Pause;
From my testing, looks like
Events.Start
is correctly interpreted asBasicEvents.Start
in the type system, so exhaustiveness checking and discriminated union refinement seem to work fine. The main thing missing is that you can't useEvents.Pause
as a type literal; you needAdvEvents.Pause
. You can usetypeof Events.Pause
and it resolves toAdvEvents.Pause
, though people on my team have been confused by that sort of pattern and I think in practice I'd encourageAdvEvents.Pause
when using it as a type.(This is for the case when you want the enum types to be assignable between each other rather than isolated enums. From my experience, it's most common to want them to be assignable.)
I think this is the neatest solution at hand, right now.
Thank you @alangpierce :+1:
any update on this?
@sdwvit I'm not one of the core people, but from my vantage point, the following syntax proposal (from OP, but re-suggested twice after that) would make everyone happy, without any known issues:
// extend enum using spread
enum AdvEvents {
...BasicEvents,
Pause = "Pause",
Resume = "Resume"
};
It would make me happy, because it would mean implementing this seemingly useful "duplicate all the members in this other enum" feature without using extends
, which I consider to be problematic for reasons I've stated. The ...
syntax avoids these issues by copying, not extending.
The issue is still marked as "Awaiting More Feedback", and I respect the core members' right to keep it in that category for as long as they feel is necessary. But also, there's nothing to stop anyone from implementing the above and submitting it as a PR.
@masak thank you for response. I now have to go through all the discussion history. Will get back to you after :)
I would absolutely love to see this happen and would absolutely love to attempt to implement this myself. However we still need to define behaviors for all enums. This all works well for string-based enums, but what about vanilla numeric enums. How does extending/copying work here?
I assume we will only want to allow extending an enum with a "same type" enum (numeric extends numeric, string extends string). Heterogeneous enums are technically supported so I suppose we should keep that support.
Should we allow extending from multiple enums? Should they all have mutually exclusive values? Or will we allow overlapping values? Priority based on lexical order?
Can the extending enums override values of the extended enums?
Must the extended enums appear that the start of the list of values or can it be in any order? I assume later defined values have higher priority?
I assume implicit numeric values will continue 1 after the max value of the extended numeric enums.
Special considerations for bit-masks?
etc. etc.
@JeffreyMercado These are good questions, and appropriate for one who hopes to attempt an implementation. :smile:
Below are my answers, guided by a "conservative" design approach (as in "let's make design decisions that disallow the cases we're not sure about, rather than making choices now that are hard to change later while staying backwards compatible").
- I assume we will only want to allow extending an enum with a "same type" enum (numeric extends numeric, string extends string)
I assume so too. The resulting enum of mixed type doesn't seem super-useful.
- Should we allow extending from multiple enums? Should they all have mutually exclusive values? Or will we allow overlapping values? Priority based on lexical order?
Since it's copying semantics we're talking about, duplicating multiple enums seems "more ok" than Multiple Inheritance à la C++. I don't immediately see a problem with it, especially if we keep building on the analogy of object spread: let newEnum = { ...enumA, ...enumB };
Should all the members have mutually exclusive values? The conservative thing would be to say "yes". Again, the analogy of object spread provides us with an alternative semantics: last one wins.
I can't think of any use cases where I would appreciate being able to override enum values. But that might just be a lack of imagination on my part. The conservative approach of disallowing collisions has the pleasing properties that it's easy to explain/internalize, and at least in theory it might expose real design errors (in fresh code, or code that's being maintained).
- Can the extending enums override values of the extended enums?
I think the answer, and the reasoning, are much the same in this case as in the previous case.
- Must the extended enums appear that the start of the list of values or can it be in any order? I assume later defined values have higher priority?
I was going to say first that this only matters if we go with the "last one wins" semantics of overriding.
But on second thought, both under "no collisions" and "last one wins", I find it weird to even want to put enum member declarations before enum spreads in the list. Like, what intent is being communicated by doing so? The spreads are a little like "imports", and these conventionally go at the top.
I don't necessarily want to forbid putting enum spreads after enum member declarations (although I think I would be fine with it being disallowed in the grammar). If it ends up being allowed, it's definitely something that linters and community convention could point out as avoidable. There's just no compelling reason to do so.
- I assume implicit numeric values will continue 1 after the max value of the extended numeric enums.
Maybe the conservative thing to do is to require an explicit value for the first member after a spread.
- Special considerations for bit-masks?
I think that would be covered by the above rule.
I was able to do something reasonable by combining enums, interfaces, and immutable objects.
export enum Unit {
SECONDS,
MINUTES,
HOURS,
DAYS,
WEEKS,
MONTHS,
YEARS,
DECADES,
CENTURIES,
MILLENNIA
}
interface Labels {
SINGULAR: Record<Unit, string>
PLURAL: Record<Unit, string>
LAST: string;
DELIM: string;
NOW: string;
}
export const EnglishLabels: Labels = {
SINGULAR: {
[Unit.SECONDS]: ' second',
[Unit.MINUTES]: ' minute',
[Unit.HOURS]: ' hour',
[Unit.DAYS]: ' day',
[Unit.WEEKS]: ' week',
[Unit.MONTHS]: ' month',
[Unit.YEARS]: ' year',
[Unit.DECADES]: ' decade',
[Unit.CENTURIES]: ' century',
[Unit.MILLENNIA]: ' millennium'
},
PLURAL: {
[Unit.SECONDS]: ' seconds',
[Unit.MINUTES]: ' minutes',
[Unit.HOURS]: ' hours',
[Unit.DAYS]: ' days',
[Unit.WEEKS]: ' weeks',
[Unit.MONTHS]: ' months',
[Unit.YEARS]: ' years',
[Unit.DECADES]: ' decades',
[Unit.CENTURIES]: ' centuries',
[Unit.MILLENNIA]: ' millennia'
},
LAST: ' and ',
DELIM: ', ',
NOW: ''
}
@illeatmyhat That's a nice use of enums, but... I fail to see how it counts as extending an existing enum. What you're doing is using the enum.
(Also, unlike with enums and switch statements, it seems that in your example you have no totality checking; someone who added an enum member later might easily forgot to add a corresponding key in the SINGULAR
and PLURAL
record in all the instances of Label
.)
@masak
someone who added an enum member later might easily forgot to add a corresponding key in the
SINGULAR
andPLURAL
record in all the instances ofLabel
.)
At least in my environment, it throws an error when an enum member is missing from either SINGULAR
or PLURAL
. The Record
type does its job, I guess.
While the documentation for TS is good, I feel that there aren't many examples of how to combine many features together in a nontrivial way. enum inheritance
was the first thing I looked up when I tried to solve internationalization problems, leading to this thread. The approach turned out to be wrong anyways, which is why I wrote this post.
@illeatmyhat
At least in my environment, it throws an error when an enum member is missing from either
SINGULAR
orPLURAL
. TheRecord
type does its job, I guess.
Oh! TIL. And yes, that does make it a lot more interesting. I see what you mean about initially reaching for enum inheritance and eventually landing on your pattern. That might not even be an isolated thing; "X/Y problems" are a real thing. More people might start with the thought "I want to extend MyEnum
", but end up using Record<MyEnum, string>
like you did.
With extends on enums, this property goes out the window.
You write,
@julian-sf: I understand and agree with the closed principal of enums (at runtime). But compile time extension would not violate the closed principle at runtime, as they would all be defined and constructed by the compiler.
I don't think that's true, because it assumes that any code that extends your enum is in your project. But a third-party module can extend your enum, and suddenly there are new enum members that are also assignable to your enum, outside of the control of code that you compile. Essentially, enums would no longer be closed, not even at compile-time.
The more I think about this, you're completely right. Enums should be closed. I really like the idea of "composing" enums as I think this is really the heart of the matter we want here 🥳.
I think this notation summarizes the concept of "stitching together" two separate enums quite elegantly:
enum ComposedEnum = { ...EnumA, ...EnumB }
So consider that my resignation on using the term extends
😆
- I assume we will only want to allow extending an enum with a "same type" enum (numeric extends numeric, string extends string). Heterogeneous enums are technically supported so I suppose we should keep that support.
I assume so too. The resulting enum of mixed type doesn't seem super-useful.
Whilst I agree that it's not useful, we SHOULD probably keep heterogeneous support for enums here. I think a linter warning would be useful here, but I don't think TS should get in the way of that. I can think of a contrived use case which is, I'm building an enum for interactions with a very poorly designed API that takes flags that are a mix of numbers and strings. Contrived, I know, but since it's allowed elsewhere, I don't think we should disallow it here.
Maybe just strong encouragement not to?
- Should we allow extending from multiple enums? Should they all have mutually exclusive values? Or will we allow overlapping values? Priority based on lexical order?
Since it's copying semantics we're talking about, duplicating multiple enums seems "more ok" than Multiple Inheritance à la C++. I don't immediately see a problem with it, especially if we keep building on the analogy of object spread: let newEnum = { ...enumA, ...enumB };
100% agree
- Should all the members have mutually exclusive values?
The conservative thing would be to say "yes". Again, the analogy of object spread provides us with an alternative semantics: last one wins.
I'm torn here. While I agree it's "best practice" to enforce mutual exclusivity of values, is it correct? It is directly contradictory to commonly-known spread semantics. One the one hand, I like the idea of enforcing mutually exclusive values, on the other hand, it breaks a lot of assumptions about how spread semantics should work. Are there any downsides to following normal spreading rules with "last one wins"? It seems like it's easier on implementation (as the underlying object is just a map anyway). But it also seems to align with common expectations. I'm leaning towards being less surprising.
There may also be good examples for wanting to override a value (although I have no idea what those would be).
But on second thought, both under "no collisions" and "last one wins", I find it weird to even want to put enum member declarations before enum spreads in the list. Like, what intent is being communicated by doing so? The spreads are a little like "imports", and these conventionally go at the top.
Well that depends, if we are following spread semantics, then it shouldn't matter what the order is. Honestly, even if we are enforcing mutually exclusive values the order wouldn't really matter, right? A collision would be an error at that point, regardless of order.
- I assume implicit numeric values will continue 1 after the max value of the extended numeric enums.
Maybe the conservative thing to do is to require an explicit value for the first member after a spread.
I agree. If you spread an enum, TS should just enforce explicit values for additional members.
@julian-sf
So consider that my resignation on using the term extends 😆
:+1: The Society for the Preservation of Sum Types cheers from the sidelines.
But on second thought, both under "no collisions" and "last one wins", I find it weird to even want to put enum member declarations before enum spreads in the list. Like, what intent is being communicated by doing so? The spreads are a little like "imports", and these conventionally go at the top.
Well that depends, if we are following spread semantics, then it shouldn't matter what the order is. Honestly, even if we are enforcing mutually exclusive values the order wouldn't really matter, right? A collision would be an error at that point, regardless of order.
I'm saying "there's no good reason to place spreads after normal member declarations"; you're saying "under the appropriate restrictions, placing them before or after makes no difference". Both of these things can be true at the same time.
The main difference in outcome seems to fall on a spectrum of allowing or disallowing spreads before normal members. It could be syntactically disallowed; it could produce a linter warning; or it could be completely fine in all regards. If the order makes no semantic difference, then it comes down to making the enum spread feature follow the principle of Least Surprise, easy to use, and easy to teach/explain.
Using the spread operator falls in the wider use of shallow copy throughout JS and TypeScript. It's certainly the more widely used and easier to understand method than using extends
, which implies a direct relationship. Creating an enum through composition would be the easier solution to consume.
Some of the work around suggestions, while valid and usable, add a lot more boilerplate code to achieve the same desired outcome. Given the final and immutable nature of an enum, creating additional enums through composition would be desirable, to maintain the properties that are consistent with other languages.
Its's just a shame that 3 years on this conversation is still going.
@jmitchell38488 I would drop a like to your comment, but your last sentence changed my mind. This is a needed discussion, since proposed solution would work, but it also implies the possibility of extending classes and interfaces this way. It is a big change which may scare some c++-like languages programmers from using typescript, since you basically end up with 2 ways of doing the same thing (class A extends B
and class A { ...(class B {}) }
). I think, both ways can be supported, but then we need extend
for enums as well for consistency.
@masak wdyt? ^
@sdwvit I'm not taking about changing the behaviour for creating classes and interfaces, I'm talking specifically about enums and creating them through composition. They're immutable final types, so we should not be able to extend in the typical inheritance fashion.
Given the nature of JS and the final transpiled value, there's no reason why composition can't be achieved. Sure would make working with enums more attractive.
Before string based enums, many would fall back to objects. Using objects also allows extending of types. For example:
When switching over to string enums, it"s impossible to achieve this without re-defining the enum.
I would be very useful to be able to do something like this:
Considering that the produced enums are objects, this won"t be too horrible either: