Typescript: 扩展基于字符串的枚举

创建于 2017-08-03  ·  68评论  ·  资料来源: microsoft/TypeScript

在基于字符串的枚举之前,许多人会退回到对象。 使用对象还允许扩展类型。 例如:

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};

const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

当切换到字符串枚举时,如果不重新定义枚举,就不可能实现这一点。

能够做这样的事情我会非常有用:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

考虑到生成的枚举是对象,这也不会太可怕:

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};
Awaiting More Feedback Suggestion

最有用的评论

所有变通方法都很好,但我希望看到 typescript 本身的枚举继承支持,以便我可以尽可能简单地使用详尽的检查。

所有68条评论

只是玩了一下,目前可以使用扩展类型的对象来做这个扩展,所以这应该可以正常工作:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
const AdvEvents = {
    ...BasicEvents,
    Pause: "Pause",
    Resume: "Resume"
};

注意,你可以接近

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);

根据您的需要,另一种选择是使用联合类型:

const enum BasicEvents {
  Start = 'Start',
  Finish = 'Finish'
}
const enum AdvEvents {
  Pause = 'Pause',
  Resume = 'Resume'
}
type Events = BasicEvents | AdvEvents;

let e: Events = AdvEvents.Pause;

缺点是你不能使用Events.Pause ; 你必须使用AdvEvents.Pause 。 如果您使用的是 const 枚举,这可能没问题。 否则,它可能不足以满足您的用例。

对于强类型的 Redux reducer,我们需要这个特性。 请在 TypeScript 中添加它。

另一种解决方法是不使用枚举,而是使用看起来像枚举的东西:

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];

所有变通方法都很好,但我希望看到 typescript 本身的枚举继承支持,以便我可以尽可能简单地使用详尽的检查。

只需使用类而不是枚举。

我只是在尝试这个。

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>;

必须有更好的方法来做到这一点。

为什么这不是一个功能? 没有重大变化,直观的行为,80 多人积极搜索并要求此功能——这似乎是一件轻而易举的事。

如果不扩展枚举,即使从命名空间中的不同文件重新导出枚举也很奇怪(并且不可能以仍然是枚举而不是对象和类型的方式重新导出枚举):

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

obrazek

+1
当前使用一种解决方法,但这应该是本机枚举功能。

我浏览了这个问题,看看是否有人提出了以下问题。 (好像没有。)

来自 OP:

enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

人们会期望AdvEvents可以分配给BasicEvents吗? (例如, extends用于类的情况。)

如果是,那么这与枚举类型意味着是最终的并且不可能扩展的事实的吻合程度如何?

@masak好点。 人们在这里想要的功能绝对不像普通的extendsBasicEvents应该分配给AdvEvents ,而不是相反。 普通extends将另一种类型细化为更具体,在这种情况下,我们希望扩展另一种类型以添加​​更多值,因此任何自定义语法都可能不应该使用extends关键字,或者至少不使用语法enum A extends B {

在那张纸条上,我确实喜欢 OP 的传播建议。

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

因为传播已经承载了原始被浅克隆到未连接副本的期望。

BasicEvents应该分配给AdvEvents ,而不是相反。

可以看到这在所有情况下都是正确的,但是如果您明白我的意思,我不确定在所有情况下都应该是正确的。 感觉它依赖于域并且依赖于那些枚举值被复制的原因。

我考虑了更多的解决方法,并且使用https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 ,您可以通过在值中定义Events来做得更好空间:

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;

根据我的测试,看起来Events.Start BasicEvents.Start ,因此详尽检查和区分联合细化似乎工作正常。 缺少的主要内容是您不能将Events.Pause用作类型文字; 你需要AdvEvents.Pause 。 您可以使用typeof Events.Pause ,它会解析为AdvEvents.Pause ,尽管我团队中的人对这种模式感到困惑,我认为在实践中我会鼓励使用AdvEvents.Pause它作为一种类型。

(这适用于您希望枚举类型可以在彼此之间分配而不是孤立的枚举的情况。根据我的经验,最常见的是希望它们是可分配的。)

另一个建议(即使它没有解决原始问题),如何使用字符串文字来创建类型联合呢?

type BEs = "Start" | "Finish";

type AEs = BEs | "Pause" | "Resume";

let example: AEs = "Finish"; // there is even autocompletion

那么,我们的问题的解决方案可能是这样的吗?

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
// { Start: string, Finish: string };


const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
} as const
// { Start: 'Start', Finish: 'Finish' };

https://github.com/Microsoft/TypeScript/pull/29510

扩展枚举应该是 TypeScript 的核心特性。 只是在说'

@wottpal重复我之前的问题:

如果 [enums can be extended],那么这与 enum 类型意味着是最终的并且不可能扩展的事实的吻合程度如何?

具体来说,在我看来,switch 语句对枚举值的整体检查取决于枚举的不可扩展性。

@masak什么? 不,它没有! 由于扩展枚举是一种更广泛的类型并且不能分配给原始枚举,因此您始终知道您使用的每个枚举的所有值。 在这种情况下扩展意味着创建一个新的枚举,而不是修改旧的枚举。

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啊,所以您的意思是extends在这里实际上具有更多的 _copying_ 语义(从AB的枚举值)? 然后,是的,开关正常。

然而,那里有_some_期望在我看来仍然是破碎的。 作为一种尝试并确定它的方法:对于类, extends不会_传达复制语义——字段和方法不会被复制到扩展的子类中; 相反,它们只是通过原型链提供。 超类中只有一个字段或方法。

因此,如果class B extends A ,我们保证B可以分配给A ,因此例如let a: A = new B();就可以了。

但是使用枚举和extends ,我们将无法做到let a: A = B.b; ,因为没有这样的相应保证。 这让我感觉很奇怪; extends在这里传达了一组关于可以做什么的假设,而枚举不符合这些假设。

然后就叫它expandsclones ? 🤷‍♂️
从用户的角度来看,基本的东西并不容易实现,这感觉很奇怪。

如果合理的语义需要一个全新的关键字(没有太多其他语言的现有技术),为什么不按照 OP 和这个评论中的建议重新使用扩展语法( ... )?

+1我希望这将被添加到默认枚举功能中。 :)

有谁知道任何优雅的解决方案??? 🧐

如果合理的语义需要一个全新的关键字(没有太多其他语言的现有技术),为什么不按照 OP 和此评论中的建议重新使用扩展语法 (...)?

是的,在考虑了更多之后,我认为这个解决方案会很好。

在阅读了整个问题主题之后,似乎有一个广泛的共识,即重用扩展运算符可以解决问题,并解决人们提出的所有关于使语法混乱/不直观的问题。

// extend enum using spread
enum AdvancedEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

在这一点上,这个问题真的还需要“等待更多反馈”标签吗, @RyanCavanaugh

想要的功能 +1

我们有关于这个问题的任何消息吗? 为枚举实现扩展运算符感觉非常有用。

特别是对于涉及元编程的用例,别名和扩展枚举的能力介于必备和必备之间。 目前没有办法以另一个名称获取一个枚举和export - 除非您采用上述解决方法之一。

@m93a啊,所以您的意思是extends在这里实际上具有更多的 _copying_ 语义(从AB的枚举值)? 然后,是的,开关正常。

然而,那里有_some_期望在我看来仍然是破碎的。 作为一种尝试并确定它的方法:对于类, extends不会_传达复制语义——字段和方法不会被复制到扩展的子类中; 相反,它们只是通过原型链提供。 超类中只有一个字段或方法。

因此,如果class B extends A ,我们保证B可以分配给A ,因此例如let a: A = new B();就可以了。

但是使用枚举和extends ,我们将无法做到let a: A = B.b; ,因为没有这样的相应保证。 这让我感觉很奇怪; extends在这里传达了一组关于可以做什么的假设,而枚举不符合这些假设。

@masak我认为你接近正确,但你做了一个不正确的小假设。 B 在enum B extends A的情况下分配给 A,因为“可分配”意味着 A 提供的所有值在 B 中都可用。当您说let a: A = B.b时,您假设 B 中的值必须是在 A 中可用,这与可分配给 A 的值不同。 let a: A = B.a是正确的,因为 B 可分配给 A。

使用以下示例中的类可以明显看出这一点:

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`

TypeScript Playground 链接

invalid access

长话短说,我相信 extends IS 是正确的术语,因为这正是正在做的事情。 在enum B extends A示例中,您总是可以期望 B 枚举包含 A 枚举的所有可能值,因为 B 是 A 的“子类”(subnum?也许有更好的词),因此可分配给 A。

所以我认为我们不需要一个新的关键字,我认为我们应该使用 extends 并且我认为这应该是 TypeScript 的一部分:D

@julian-sf 我想我同意你写的每一件事......

...但是... :slightly_smiling_face:

当我在这里提出问题时,这种情况呢?

// example from OP
enum BasicEvents {
    Start = "Start",
    Finish = "Finish"
};

// extend enum using "extends" keyword
enum AdvEvents extends BasicEvents {
    Pause = "Pause",
    Resume = "Resume"
};

鉴于PauseAdvEventsAdvEvents _extends_ BasicEvents的_instance_,您是否还期望PauseBasicEvents的_instance_

另一方面,枚举(恕我直言)的核心价值主张是它们是_封闭_/“最终”(如不可扩展),因此像switch语句这样的东西可以假设整体。 (所以AdvEvents能够 _extend_ 成为BasicEvent的含义违反了枚举的某种最小惊喜。)

我认为您不能超过以下三个属性中的两个:

  • 枚举被关闭/最终/可预测的总数
  • 两个enum声明之间的extends关系
  • (合理的)假设,如果bBB extends A的实例,那么bA的实例

@masak我理解并同意枚举的封闭原则(在运行时)。 但是编译时扩展不会违反运行时的封闭原则,因为它们都是由编译器定义和构造的。

(合理的)假设如果 b 是 B 的实例并且 B 扩展 A,则 b 是 A 的实例

我认为这种推理有点误导,因为实例/类二分法并不能真正分配给枚举。 枚举不是类,它们没有实例。 但是,我确实认为,如果做得好,它们可以是可扩展的。 把枚举想象成集合。 在此示例中,B 是 A 的超集。因此可以合理地假设 A 中的任何值都存在于 B 中,但只有 B 的某些值会存在于 A 中。

我知道担忧来自哪里……不过。 我不知道该怎么做。 枚举扩展问题的一个很好的例子:

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...

在这种情况下,枚举的行为与类完全不同。 如果这些是类,您会期望能够很容易地将 B 转换为 A,但这显然在这里行不通。 我不一定认为这很糟糕,我认为它应该被考虑在内。 IE,您不能像类一样在其继承树中向上限定枚举类型。 这可以通过编译器错误来解释,即“无法将超集枚举 B 分配给 A,因为并非 B 的所有值都存在于 A 中”。

@julian-sf

我认为这种推理有点误导,因为实例/类二分法并不能真正分配给枚举。 枚举不是类,它们没有实例。

从表面上看,你是绝对正确的。

  • 作为一种独立的语言结构,枚举具有_members_,而不是实例。 手册和语言规范都使用术语“成员”。 (C# 和 Python 类似地使用术语“成员”。Java 使用“枚举常量”,因为“成员”在 Java 中具有重载含义。)
  • 从编译代码的角度来看,枚举具有_properties_,映射两种方式——名称到值和值到名称。 同样,不是实例。

考虑到这一点,我意识到 Java 对枚举的看法让我有点受宠若惊。 在 Java 中,枚举值实际上是其枚举类型的实例。 在实现方面,枚举是扩展Enum 类的类。 (你不能手动做,你必须通过enum关键字,但这就是幕后发生的事情。)这样做的好处是枚举获得了所有便利类所做的事情:它们可以有字段、构造函数、方法......在这种方法中,枚举成员_are_实例。 (JLS 说了这么多。)

请注意,我不建议对 TypeScript 枚举语义进行任何更改。 特别是,我并不是说 TypeScript 应该改为使用 Java 的枚举模型。 我_am_说在枚举/枚举成员之上覆盖一个类/实例“理解”是有启发性/有见地的。 不是“一个枚举_is_一个类”或“一个枚举成员_is_一个实例”......但有一些相似之处可以延续。

有什么相似之处? 首先,输入会员资格。

enum Foo { A, B, C }
enum Bar { X, Y, Z }

let foo: Foo = Foo.C;
foo = Bar.Z;

最后一行不进行类型检查,因为Bar.Z不是Foo 。 同样,这_实际上_不是类和实例,但可以_理解_使用相同的模型,好像FooBar是类,六个成员是它们各自的实例。

(为了这个论点的目的,我们将忽略let foo: Foo = 2;在语义上也是合法的事实,并且通常number值可以分配给枚举类型的变量。)

枚举具有它们是_封闭_的附加属性——抱歉,我不知道更好的术语——一旦你定义了它们,你就不能扩展它们。 具体来说,枚举声明中列出的成员是与枚举类型匹配的_only_事物。 (在“封闭世界假设”中“封闭”。)这是一个很好的属性,因为您可以完全确定地验证枚举中switch语句中的所有情况都已涵盖。

在枚举上使用extends ,这个属性就消失了。

你写,

我理解并同意枚举的封闭原则(在运行时)。 但是编译时扩展不会违反运行时的封闭原则,因为它们都是由编译器定义和构造的。

我认为这不是真的,因为它假定扩展枚举的任何代码都在您的项目中。 但是第三方模块可以扩展您的枚举,并且突然有 _new_ 枚举成员也可以分配给您的枚举,超出了您编译的代码的控制范围。 本质上,枚举将不再被关闭,即使在编译时也是如此。

我仍然觉得我在表达我的意思时有些笨拙,但我相信这很重要: extends enum会破坏枚举最宝贵的特性之一,即它们'重新关闭。 正是出于这个原因,请计算有多少种语言绝对_禁止_扩展/子类化枚举。

我考虑了更多的解决方法,并且使用#17592 (comment) ,您可以通过在值空间中定义Events来做得更好:

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;

根据我的测试,看起来Events.Start BasicEvents.Start ,因此详尽检查和区分联合细化似乎工作正常。 缺少的主要内容是您不能将Events.Pause用作类型文字; 你需要AdvEvents.Pause 。 您_可以_使用typeof Events.Pause ,它会解析为AdvEvents.Pause ,尽管我团队中的人对这种模式感到困惑,我认为在实践中我会鼓励使用AdvEvents.Pause它作为一种类型。

(这适用于您希望枚举类型可以在彼此之间分配而不是孤立的枚举的情况。根据我的经验,最常见的是希望它们是可分配的。)

我认为这是目前最简洁的解决方案。

谢谢@alangpierce :+1:

这事有进一步更新吗?

@sdwvit我不是核心人员之一,但从我的角度来看,以下语法建议(来自 OP,但之后重新建议了两次)会让每个人都开心,没有任何已知问题:

// extend enum using spread
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause",
    Resume = "Resume"
};

这会让 _me_ 高兴,因为这意味着使用extends实现这个看似有用的“复制另一个枚举中的所有成员”功能_without_,我认为这是有问题的,原因我已经说明了。 ...语法通过复制而不是扩展来避免这些问题。

该问题仍被标记为“等待更多反馈”,我尊重核心成员将其保留在该类别中的权利,只要他们认为有必要。 但是,没有什么可以阻止任何人实施上述内容并将其作为 PR 提交。

@masak谢谢你的回复。 我现在必须浏览所有讨论历史。 之后会回复你的:)

我非常希望看到这种情况发生,并且非常希望自己尝试实现这一点。 然而,我们仍然需要为所有枚举定义行为。 这一切都适用于基于字符串的枚举,但对于普通数字枚举呢? 扩展/复制在这里如何工作?

  • 我假设我们只想允许使用“相同类型”枚举扩展枚举(数字扩展数字,字符串扩展字符串)。 异构枚举在技术上得到支持,所以我想我们应该保持这种支持。

  • 我们应该允许从多个枚举扩展吗? 它们都应该具有相互排斥的价值吗? 或者我们会允许重叠值吗? 基于词汇顺序的优先级?

  • 扩展枚举可以覆盖扩展枚举的值吗?

  • 扩展枚举必须出现在值列表的开头还是可以以任何顺序出现? 我假设以后定义的值具有更高的优先级?

  • 我假设隐式数值将在扩展数值枚举的最大值之后继续 1。

  • 位掩码的特殊注意事项?

等等等等

@JeffreyMercado这些都是很好的问题,适合希望尝试实施的人。 :微笑:

以下是我的答案,以“保守”设计方法为指导(如“让我们做出不允许我们不确定的情况的设计决策,而不是现在做出以后难以更改的选择,同时保持向后兼容”)。

  • 我假设我们只想允许使用“相同类型”枚举扩展枚举(数字扩展数字,字符串扩展字符串)

我也这么认为。 由此产生的混合类型枚举似乎不是很有用。

  • 我们应该允许从多个枚举扩展吗? 它们都应该具有相互排斥的价值吗? 或者我们会允许重叠值吗? 基于词汇顺序的优先级?

由于它是复制我们正在谈论的语义,因此复制多个枚举似乎比 C++ 中的多重继承“更好”。 我没有立即发现它有什么问题,特别是如果我们继续建立对象传播的类比: let newEnum = { ...enumA, ...enumB };

所有成员都应该具有互斥的值吗? 保守的做法是说“是”。 同样,对象传播的类比为我们提供了另一种语义:最后一个获胜。

我想不出任何我希望能够覆盖枚举值的用例。 但这可能只是我缺乏想象力。 不允许冲突的保守方法具有易于解释/内部化的令人愉悦的特性,并且至少在理论上它可能会暴露真正的设计错误(在新代码或正在维护的代码中)。

  • 扩展枚举可以覆盖扩展枚举的值吗?

我认为这个案例的答案和推理与前一个案例大致相同。

  • 扩展枚举必须出现在值列表的开头还是可以以任何顺序出现? 我假设以后定义的值具有更高的优先级?

我首先要说的是,这只有在我们采用“最后一个获胜”的覆盖语义时才重要。

但是再想一想,无论是在“没有冲突”还是“最后一个获胜”下,我都觉得甚至_想要_在列表中枚举传播之前放置枚举成员声明都很奇怪。 比如,这样做传达了什么意图? 点差有点像“进口”,通常位于顶部。

我不一定要禁止在枚举成员声明之后放置枚举传播(尽管我认为在语法中不允许使用它会很好)。 如果它最终被允许,这绝对是 linter 和社区惯例可以指出的可以避免的事情。 没有令人信服的理由这样做。

  • 我假设隐式数值将在扩展数值枚举的最大值之后继续 1。

也许保守的做法是在传播后要求第一个成员的明确值。

  • 位掩码的特殊注意事项?

我认为上述规则将涵盖这一点。

通过组合枚举、接口和不可变对象,我能够做一些合理的事情。

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这是枚举的一个很好的用途,但是......我看不出它如何算作扩展现有枚举。 你正在做的是_使用_枚举。

(此外,与枚举和 switch 语句不同,在您的示例中,您似乎没有进行总体检查;后来添加枚举成员的人可能很容易忘记在SINGULARPLURAL中添加相应的键Label的所有实例中。)

@masak

稍后添加枚举成员的人可能很容易忘记在 Label 的所有实例中的Label SINGULARPLURAL记录中添加相应的键。)

至少在我的环境中,当SINGULARPLURAL中缺少枚举成员时,它会引发错误。 Record类型完成了它的工作,我猜。

虽然 TS文档很好,但我觉得没有很多例子可以说明如何以一种重要的方式将许多功能组合在一起。 enum inheritance是我在尝试解决国际化问题时查找的第一件事,导致了这个线程。 无论如何,这种方法被证明是错误的,这就是我写这篇文章的原因。

@illeatmyhat

至少在我的环境中,当SINGULARPLURAL中缺少枚举成员时,它会引发错误。 Record类型完成了它的工作,我猜。

哦! 直到。 是的,这确实使它更有趣。 我明白你最初达到枚举继承并最终登陆你的模式的意思。 这甚至可能不是一件孤立的事情。 “X/Y 问题”是真实存在的。 更多的人可能会从“我想扩展MyEnum ”的想法开始,但最终会像你一样使用Record<MyEnum, string>

回复@masak

通过对枚举进行扩展,这个属性就消失了。

你写,

@julian-sf:我理解并同意枚举的封闭原则(在运行时)。 但是编译时扩展不会违反运行时的封闭原则,因为它们都是由编译器定义和构造的。

我认为这不是真的,因为它假定扩展枚举的任何代码都在您的项目中。 但是第三方模块可以扩展您的枚举,并且突然有新的枚举成员也可以分配给您的枚举,不受您编译的代码的控制。 本质上,枚举将不再被关闭,即使在编译时也是如此。

我越想这个,你是完全正确的。 枚举应该关闭。 我真的很喜欢“组合”枚举的想法,因为我认为这确实是我们想要的问题的核心🥳。

我认为这个符号非常优雅地总结了“拼接”两个独立枚举的概念:

enum ComposedEnum = { ...EnumA, ...EnumB }

所以考虑一下我辞职使用这个词extends 😆


评论@masak@JeffreyMercado问题的回答:

  • 我假设我们只想允许使用“相同类型”枚举扩展枚举(数字扩展数字,字符串扩展字符串)。 异构枚举在技术上得到支持,所以我想我们应该保持这种支持。

我也这么认为。 由此产生的混合类型枚举似乎不是很有用。

虽然我同意它没有用,但我们应该在这里保留对枚举的异构支持。 我认为 linter 警告在这里会很有用,但我认为 TS 不应该妨碍它。 我可以想到一个人为的用例,即我正在构建一个枚举,用于与设计非常糟糕的 API 进行交互,该 API 采用数字和字符串混合的标志。 做作,我知道,但既然它在其他地方是允许的,我认为我们不应该在这里禁止它。

也许只是强烈的鼓励不要?

  • 我们应该允许从多个枚举扩展吗? 它们都应该具有相互排斥的价值吗? 或者我们会允许重叠值吗? 基于词汇顺序的优先级?

由于它是复制我们正在谈论的语义,因此复制多个枚举似乎比 C++ 中的多重继承“更好”。 我没有立即发现它有什么问题,特别是如果我们继续建立对象传播的类比: let newEnum = { ...enumA, ...enumB };

100% 同意

  • 所有成员都应该具有互斥的值吗?

保守的做法是说“是”。 同样,对象传播的类比为我们提供了另一种语义:最后一个获胜。

我在这里被撕裂了。 虽然我同意强制价值的相互排他性是“最佳实践”,但它是否正确? 它与众所周知的传播语义直接矛盾。 一方面,我喜欢强制互斥值的想法,另一方面,它打破了许多关于扩展语义应该如何工作的假设。 遵循“最后一个获胜”的正常传播规则有什么缺点吗? 似乎实现起来更容易(因为底层对象只是一张地图)。 但它似乎也符合共同的期望。 我倾向于不那么令人惊讶。

也可能有想要覆盖一个值的好例子(虽然我不知道那些会是什么)。

但是再想一想,无论是在“没有冲突”还是“最后一个获胜”下,我什至想在列表中的枚举传播之前放置枚举成员声明都很奇怪。 比如,这样做传达了什么意图? 点差有点像“进口”,通常位于顶部。

好吧,这取决于,如果我们遵循传播语义,那么顺序是什么都无关紧要。 老实说,即使我们强制执行互斥值,顺序也不重要,对吧? 无论顺序如何,碰撞都将是一个错误。

  • 我假设隐式数值将在扩展数值枚举的最大值之后继续 1。

也许保守的做法是在传播后要求第一个成员的明确值。

我同意。 如果您传播枚举,TS 应该只为其他成员强制执行显式值。

@julian-sf

所以考虑到我对使用这个词的辞职延长了😆

:+1: 和类型保存协会在一旁欢呼。

但是再想一想,无论是在“没有冲突”还是“最后一个获胜”下,我什至想在列表中的枚举传播之前放置枚举成员声明都很奇怪。 比如,这样做传达了什么意图? 点差有点像“进口”,通常位于顶部。

好吧,这取决于,如果我们遵循传播语义,那么顺序是什么都无关紧要。 老实说,即使我们强制执行互斥值,顺序也不重要,对吧? 无论顺序如何,碰撞都将是一个错误。

我的意思是“没有充分的理由在正常成员声明之后放置点差”; 您是在说“在适当的限制下,将它们放在之前或之后没有区别”。 这两件事可以同时为真。

结果的主要区别似乎在于在正常成员之前允许或不允许点差。 它可能在语法上是不允许的; 它可能会产生 linter 警告; 或者它可能在所有方面都很好。 如果顺序没有语义差异,那么归结为使枚举扩展功能遵循Least Surprise原则,易于使用,易于教授/解释。

使用扩展运算符属于在整个 JS 和 TypeScript 中更广泛地使用浅拷贝。 它肯定是比使用extends更广泛使用和更容易理解的方法,这意味着直接关系。 通过组合创建枚举将是更容易使用的解决方案。

围绕建议的一些工作虽然有效且可用,但添加了更多样板代码以实现相同的预期结果。 鉴于枚举的最终和不可变的性质,通过组合创建额外的枚举是可取的,以保持与其他语言一致的属性。

遗憾的是,这场对话持续了 3 年。

@jmitchell38488我想对你的评论点个赞,但你的最后一句话改变了我的想法。 这是一个必要的讨论,因为提出的解决方案会起作用,但它也意味着以这种方式扩展类和接口的可能性。 这是一个很大的变化,可能会吓到一些类似 c++ 的语言程序员使用 typescript,因为你基本上最终会以两种方式来做同样的事情( class A extends Bclass A { ...(class B {}) } )。 我认为,两种方式都可以支持,但是我们需要extend用于枚举以及一致性。

@masak wdyt? ^

@sdwvit我不打算改变创建类和接口的行为,我专门谈论枚举并通过组合创建它们。 它们是不可变的最终类型,因此我们不应该能够以典型的继承方式进行扩展。

鉴于 JS 的性质和最终的转译值,没有理由无法实现组合。 当然会使使用枚举更具吸引力。

@masak wdyt? ^

嗯。 我认为语言一致性是一个值得称赞的目标,因此在类和接口中要求类似的...特性并不是_先验_错误。 但我认为那里的情况较弱或不存在,原因有两个:(a)类和接口已经具有扩展机制,并且添加第二个提供的额外价值很小(而为枚举提供一个将涵盖人们的用例多年来一直回到这个问题); (b) 向类添加新的语法和语义具有更高的批准门槛,因为类在某种意义上来自 EcmaScript 规范。 (TypeScript 比 ES6 更老,但一直在积极努力让前者与后者保持接近。这包括不引入额外的功能。)

我认为这个线程已经开放了很长时间,只是因为它代表了一个有价值的功能,可以涵盖实际的用例,但它还没有得到一个 PR。 做这样的公关需要时间和精力,不仅仅是说你想要这个功能。 :眨眼:

有人在研究这个功能吗?

有人在研究这个功能吗?

我的猜测是没有,因为我们甚至还没有完成关于它的讨论,哈哈!

我确实觉得我们已经接近达成共识。 由于这将是一种语言添加,因此可能需要“拥护者”来推动该提案。 我认为 TS 团队的某个人需要进来并将问题从“等待反馈”转移到“等待提案”(或类似的东西)。

我正在研究它的原型。 虽然由于缺乏时间和不熟悉代码的结构,我还没有走得太远。 我确实希望看到这一点,如果没有其他动作,我会尽可能继续。

也会喜欢这个功能。 37个月过去了,希望尽快取得进展

近期会议记录:

  • 我们喜欢扩展语法,因为extends意味着一个子类型,而“扩展”一个枚举会创建一个超类型。

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • 想法是AdvEvents.Start将解析为与BasicEvents.Start相同的类型标识。 这意味着BasicEvents.StartAdvEvents.Start类型可以相互分配,而BasicEvents类型可以分配给AdvEvents 。 希望这具有直观的意义,但重要的是要注意,这意味着传播不仅仅是一种语法快捷方式——如果我们将传播扩展成它看起来的意思:

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     Start = "Start",
     Finish = "Finish",
     Pause = "Pause",
     Resume = "Resume"
    }
    

    这有不同的行为——这里,由于字符串枚举的不透明质量, BasicEvents.StartAdvEvents.Start _not_ 可以相互分配。

    此实现的另一个次要后果是AdvEvents.Start可能会在快速信息和声明发出中序列化为BasicEvents.Start (至少在没有将成员链接到AdvEvents的语法上下文的情况下——将鼠标悬停在文字表达式AdvEvents.Start上可能会导致快速信息显示AdvEvents.Start ,但显示BasicEvents.Start可能更清楚)。

  • 这只会在字符串枚举中被允许。

我想试试这个。

澄清:这未被批准实施。

这里有两种可能的行为,它们看起来都很糟糕。

选项1:实际上是糖

如果 spread 真的与复制扩展的枚举成员的含义相同,那么当人们尝试使用扩展枚举的值就好像它是扩展枚举的值并且它不起作用时,将会有一个很大的惊喜。

选项2:它实际上不是糖

首选选项是 spread 更像@aj-r 在线程顶部附近的联合类型建议。 如果这是人们已经想要的行为,那么为了便于理解,桌面上的现有选项似乎是绝对可取的。 否则,我们将创建一种新的字符串枚举,它的行为与任何其他字符串枚举不同,这很奇怪,似乎破坏了这里建议的“简单性”。

人们想要哪种行为,为什么?

我不想要选项 1,因为它会带来很大的惊喜。

我确实想要选项 2,但我希望有足够的语法支持来克服@aj-r 提到的缺点,所以我可以从他的示例中编写let e: Events = Events.Pause; 。 缺点并不可怕,但它是扩展枚举无法隐藏实现的地方; 所以这有点恶心。

我也认为选项 1 是个坏主意。 我在我的公司搜索了对这个问题的引用,发现了两个链接的代码审查,在这两种情况下(以及我个人的经验),显然需要将较小枚举的元素分配给较大的枚举类型. 我特别担心有人介绍...认为它的行为类似于选项 2,然后当更复杂的情况不起作用时,下一个人会变得非常困惑(或诉诸诸如as unknown as Events.Pause之类的技巧)。

已经有很多方法可以尝试获得选项 2 的行为:此线程中有大量片段,以及涉及字符串文字联合的各种方法。 我对实现选项 1 的最大担忧是,它有效地引入了另一种获取选项 2 的错误方法,从而导致学习这部分 TypeScript 的人更多的故障排除和挫败感。

人们想要哪种行为,为什么?

由于代码胜于雄辩,并使用 OP 中的示例:

const BasicEvents = {
    Start: "Start",
    Finish: "Finish"
};
enum AdvEvents {
    ...BasicEvents,
    Pause = "Pause", // We added a new field
    Finish = "Finish2" // Oops, we actually modified a field in the parent enum
};
// The TypeScript compiler should refuse to compile this code
// But after removing the "Finish2" line,
// the TypeScript compiler should successfully handle it as one would normally expect with the spread operator

这个例子展示了选项 2,对吧? 如果是这样,那么我非常想要选项2。

否则,我们将创建一种新的字符串枚举,它的行为与任何其他字符串枚举不同,这很奇怪,似乎破坏了此处建议的“简单性”

我同意选项 2 有点令人不安,最好退后一步想想替代方案。 这是对如何在不向当今的枚举添加任何新语法或行为的情况下完成它的探索:

我认为我在https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 中的建议最近得到了最接近的结果,并且可能需要解决:

type Events = BasicEvents | AdvEvents;
const Events = {...BasicEvents, ...AdvEvents};

我发现这种方法有两个主要问题:

  • 学习 TypeScript 的人真的很困惑; 如果您对类型空间与值空间没有扎实的掌握,它看起来就像是用常量覆盖了一个类型。
  • 它不完整,因为它不允许您使用Events.Pause作为类型。

我之前建议https://github.com/microsoft/TypeScript/issues/29130将(除其他外)解决第二个要点,我认为它仍然很有价值,尽管它肯定会增加很多微妙之处名称是如何工作的。

我认为可以解决这两点的一个语法想法是另一种const语法,它也声明了一个类型:

// Declares a value and a type at the same time with the same name (just like `enum` and `class` already do).
// Requires the right-hand side to be either a unit type or an object where all values are unit types.
// The JS emit would just take out the word "type" and leave everything else.
const type Events = {...BasicEvents, ...AdvEvents};
...
const e: Events.Pause = Events.Pause;
...
// The syntax could also make this pattern more ergonomic.
const type INACCESSIBLE = "INACCESSIBLE";
const response: {name: string, favoriteColor: string} | INACCESSIBLE = INACCESSIBLE;

这将更接近一个根本不需要枚举的世界。 (对我来说,枚举总是觉得它们违背了 TS 设计目标,因为它们是一种表达式级语法,具有非平凡的发射行为并且默认情况下是名义上的。) const type声明语法也可以让你创建一个结构类型的字符串枚举,如下所示:

const type BasicEvents = {
  Start: 'Start',
  Finish: 'Finish',
};  // "as const" would be implicit for "const type" declarations.

诚然,这需要工作的方式是类型BasicEventstypeof BasicEvents[keyof typeof BasicEvents]的简写,这对于像const type这样的通用命名语法可能过于集中。 也许不同的关键字会更好。 太糟糕const enum已经被占用了😛。

从那里开始,我认为(字符串)枚举和对象文字之间的唯一差距是名义类型。 这可能可以使用诸如as uniqueconst unique type之类的语法来解决,这些语法基本上为这些对象类型选择类似于枚举的名义类型行为,理想情况下也适用于常规字符串常量。 在定义Events时,这也将在选项 1 和选项 2 之间做出明确的选择:当您打算Events成为完全不同的类型(选项 1)时,您使用unique ,当您想要EventsBasicEvents之间的可分配性(选项 2)时,您省略unique

使用const typeconst unique type ,将有一种方法可以干净地合并现有枚举,也可以将枚举表示为 TS 功能的自然组合,而不是一次性的。

这里发生了什么事? 😅

哇,从 2017 年开始🤪,您还需要什么反馈?

你还需要什么反馈?

就在这儿??? 我们不只是实施建议,因为它们已经过时了!

你还需要什么反馈?

就在这儿??? 我们不只是实施建议,因为它们已经过时了!

是的。 我不确定它是哪种方式。

再次阅读并看到 #40998 我认为这是最好的方法...... emun 是对象,我认为传播更容易合并/扩展枚举。

我认为我没有足够的资格就语言设计发表意见,但我认为我可以作为普通开发人员提供反馈。

几周前,我在一个想要使用此功能的真实项目中遇到了这个问题。 我最终使用了@alangpierce方法。 不幸的是,由于我对雇主的责任,我不能在这里分享代码,但这里有几点:

  1. 重复声明(新枚举的 type 和 const )不是一个大问题,也不会对可读性造成太大损害。
  2. 在我的例子中,枚举代表了特定算法的不同动作,并且在该算法中针对不同情况有不同的动作集。 使用类型层次结构使我能够验证某些操作不会在编译时发生:这是整个事情的重点,结果证明它非常有用。
  3. 我编写的代码是一个内部库,虽然不同枚举类型之间的区别在库内部很重要,但对于该库的外部用户来说并不重要。 使用这种方法,我只能导出一种类型,即内部所有不同枚举的总和类型,并隐藏实现细节。
  4. 不幸的是,我无法找到一种惯用且易于阅读的方法来从类型声明中自动解析 sum 类型的值。 (就我而言,我提到的不同算法步骤是在运行时从 SQL 数据库加载的)。 这不是一个大问题(手动编写解析代码很简单),但如果枚举继承的实现能够关注这个问题,那就太好了。

总的来说,我认为很多业务逻辑枯燥的实际项目会从这个功能中受益匪浅。 将枚举拆分为不同的子类型将允许类型系统检查许多现在由单元测试检查的不变量,并且使类型系统无法表示不正确的值总是一件好事。

你好,

让我在这里加两分钱🙂

我的背景

我有一个 API,其中包含使用 tsoa 生成的OpenApi文档。

我的一个模型的状态定义如下:

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

我有一个方法setStatus需要这些状态的一个子集。 由于该功能不可用,我考虑以这种方式复制枚举:

enum RequestedEntityStatus {
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
}

所以我的方法是这样描述的:

public setStatus(status: RequestedEntityStatus) {
   this.status = status;
}

使用该代码,我收到此错误:

Conversion of type 'RequestedEntityStatus' to type 'EntityStatus' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

我现在会这样做,但很好奇并开始搜索这个存储库,当我发现这个时。
一直向下滚动后,我没有找到任何人(或者我可能错过了一些东西)建议这个用例。

在我的用例中,我不想从枚举“扩展”,因为 EntityStatus 没有理由成为 RequestedEntityStatus 的扩展。 我希望能够从更通用的枚举中“挑选”。

我的建议

我发现扩展运算符比扩展提议更好,但我想更进一步并提出以下建议:

enum EntityStatus {
    created = 'created',
    started = 'started',
    paused = 'paused',
    stopped = 'stopped',
    archived = 'archived',
}

enum RequestedEntityStatus {
    // Pick/Reuse from EntityStatus
    EntityStatus.started,
    EntityStatus.paused,
    EntityStatus.stopped,
}

// Fake enum, just to demonstrate
enum TargetStatus {
    {...RequestedEntityStatus},
    // Why not another spread here?
    //{...AnotherEnum},
    EntityStatus.archived,
}

public class Entity {
    private status: EntityStatus = 'created'; // Why not a cast here, if types are compatible, and deserializable from a JSON. EntityStatus would just act as a type union here.

    public setStatus(requestedStatus: RequestedEntityStatus) {
        if (this.status === (requestedStatus as EntityStatus)) { // Should be OK because types are compatible, but the cast would be needed to avoid comparing oranges and apples
            return;
        }

        if (requestedStatus == RequestedStatus.stopped) { // Should be accessible from the enum as if it was declared inside.
            console.log('Stopping...');
        }

        this.status = requestedStatus;// Should work, since EntityStatus contains all the enum members that RequestedEntityStatus has.
    }

    public getStatusAsStatusRequest() : RequestedEntityStatus {
        if (this.status === EntityStatus.created || this.status === EntityStatus.archived) {
            throw new Error('Invalid status');
        }
        return this.status as RequestedEntityStatus; // We have  eliminated the cases where the conversion is impossible, so the conversion should be possible now.
    }
}

更一般地说,这应该有效:

enum A { a = 'a' }
enum B { a = 'a' }

const a:A = A.a;
const b:B = B.a;

console.log(a === b);// Should not say "This condition will always return 'false' since the types 'A' and 'B' have no overlap.". They do have overlaps

换一种说法

我想放宽对枚举类型的约束,使其表现得更像联合( 'a' | 'b' )而不是不透明的结构。

通过将这些能力添加到编译器,两个具有相同值的独立枚举可以使用与联合相同的规则相互分配:
给定以下枚举:

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C { ...A, c = 'c' }

还有三个变量a:Ab:Bc:C

  • c = a应该可以工作,因为 A 只是 C 的一个子集,所以 A 的每个值都是 C 的有效值
  • c = b应该可以工作,因为 B 只是'a' | 'c'两者都是 C 的有效值
  • 如果已知a'b'不同(仅等于'a'类型),则b = a可以工作
  • 同样,如果b'c'不同,$ a = b也可以工作
  • 如果已知c不同于'b' (这将等于'a'|'c' ,这正是 B 的含义), b = c可以工作

或者我们可能需要在右侧进行显式转换,至于相等比较?

关于枚举成员冲突

我不是“最后胜利”规则的粉丝,即使使用传播运算符感觉很自然。
我想说的是,如果枚举的“键”或“值”重复,编译器应该返回一个错误,除非键和值都相同:

enum A { a = 'a', b = 'b' }
enum B { a = 'a' , c = 'c' }
enum C {...A, ...B } // OK, equivalent to enum C { a = 'a', b = 'b', c = 'c' }, a is deduplicated
enum D {...A, a = 'd' } // Error : Redefinition of a
enum E {...A, d = 'a' } // Error : Duplicate key with value 'a'

结束

我发现这个提议对于 TS 开发人员来说非常灵活和自然,同时允许更多的类型安全(与as unknown as T相比)而无需实际更改枚举是什么。 它只是引入了一种将成员添加到枚举的新方法,以及一种将枚举相互比较的新方法。
你怎么看? 我是否错过了一些明显的语言架构问题,导致此功能无法实现?

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

xealot picture xealot  ·  150评论

rbuckton picture rbuckton  ·  139评论

quantuminformation picture quantuminformation  ·  273评论

born2net picture born2net  ·  150评论

tenry92 picture tenry92  ·  146评论