Typescript: Ampliación de enumeraciones basadas en cadenas

Creado en 3 ago. 2017  ·  68Comentarios  ·  Fuente: microsoft/TypeScript

Antes de las enumeraciones basadas en cadenas, muchas recurrían a los objetos. El uso de objetos también permite la extensión de tipos. Por ejemplo:

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

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

Al cambiar a enumeraciones de cadena, es imposible lograr esto sin volver a definir la enumeración.

Me sería muy útil poder hacer algo como esto:

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

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

Teniendo en cuenta que las enumeraciones producidas son objetos, esto tampoco será demasiado horrible:

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

Comentario más útil

Todas las soluciones están bien, pero me gustaría ver el soporte de herencia de enumeración del propio mecanografiado para poder usar controles exhaustivos de la manera más simple posible.

Todos 68 comentarios

Solo jugué un poco con él y actualmente es posible hacer esta extensión usando un objeto para el tipo extendido, por lo que debería funcionar bien:

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

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

Tenga en cuenta que puede acercarse a

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

Otra opción, dependiendo de tus necesidades, es utilizar un tipo de unión:

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

let e: Events = AdvEvents.Pause;

La desventaja es que no puedes usar Events.Pause ; tienes que usar AdvEvents.Pause . Si está utilizando enumeraciones const, esto probablemente esté bien. De lo contrario, podría no ser suficiente para su caso de uso.

Necesitamos esta función para los reductores Redux fuertemente tipados. Por favor, agréguelo en TypeScript.

Otra solución es no usar enumeraciones, sino usar algo que parezca una enumeración:

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

Todas las soluciones están bien, pero me gustaría ver el soporte de herencia de enumeración del propio mecanografiado para poder usar controles exhaustivos de la manera más simple posible.

Simplemente use la clase en lugar de la enumeración.

Estaba probando esto.

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

Tiene que haber una mejor manera de hacer esto.

¿Por qué no es esta una característica ya? Sin cambios de última hora, comportamiento intuitivo, más de 80 personas que buscaron y demandaron activamente esta función: parece una obviedad.

Incluso volver a exportar la enumeración desde un archivo diferente en un espacio de nombres es realmente extraño sin extender las enumeraciones (y es imposible volver a exportar la enumeración de una manera que todavía es una enumeración y no un objeto y tipo):

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
Actualmente se usa una solución alternativa, pero esta debería ser una función de enumeración nativa.

Revisé este problema para ver si alguien había planteado la siguiente pregunta. (Parece que no.)

Desde OP:

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

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

¿La gente esperaría que AdvEvents fuera asignable a BasicEvents ? (Como es, por ejemplo, el caso de extends para las clases).

En caso afirmativo, ¿qué tan bien encaja eso con el hecho de que los tipos de enumeración están destinados a ser definitivos y no es posible extenderlos?

@masak gran punto. La característica que la gente quiere aquí definitivamente no es como extends normal. BasicEvents debe ser asignable a AdvEvents , no al revés. Normal extends refina otro tipo para que sea más específico, y en este caso queremos ampliar el otro tipo para agregar más valores, por lo que cualquier sintaxis personalizada para esto probablemente no debería usar la palabra clave extends , o al menos no usar la sintaxis enum A extends B { .

En ese sentido, me gustó la sugerencia de difusión para esto de OP.

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

Porque la difusión ya conlleva la expectativa de que el original sea clonado superficialmente en una copia desconectada.

BasicEvents debe ser asignable a AdvEvents , no al revés.

Puedo ver cómo eso podría ser cierto en todos los casos, pero no estoy seguro de que deba ser cierto en todos los casos, si entiendes lo que quiero decir. Parece que dependería del dominio y dependería de la razón por la que se copiaron esos valores de enumeración.

Pensé un poco más en las soluciones alternativas y trabajando con https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 , puede hacerlo un poco mejor definiendo también Events en el valor espacio:

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;

De mis pruebas, parece que Events.Start se interpreta correctamente como BasicEvents.Start en el sistema de tipos, por lo que la verificación exhaustiva y el refinamiento de unión discriminado parecen funcionar bien. Lo principal que falta es que no puede usar Events.Pause como un tipo literal; necesitas AdvEvents.Pause . Puede usar typeof Events.Pause y se resuelve en AdvEvents.Pause , aunque la gente de mi equipo se ha confundido con ese tipo de patrón y creo que en la práctica recomendaría AdvEvents.Pause al usar es como un tipo.

(Esto es para el caso cuando desea que los tipos de enumeración se puedan asignar entre sí en lugar de enumeraciones aisladas. Según mi experiencia, es más común querer que se puedan asignar).

Otra sugerencia (aunque no resuelve el problema original), ¿qué tal usar literales de cadena para crear una unión de tipo en su lugar?

type BEs = "Start" | "Finish";

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

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

Entonces, ¿la solución a nuestros problemas podría ser esta?

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

La extensión de enumeraciones debería ser una característica central de TypeScript. Sólo digo'

@wottpal Repitiendo mi pregunta anterior:

Si [las enumeraciones se pueden extender], ¿qué tan bien encaja eso con el hecho de que los tipos de enumeración están destinados a ser definitivos y no es posible extenderlos?

Específicamente, me parece que la verificación de la totalidad de una declaración de cambio sobre un valor de enumeración depende de la no extensibilidad de las enumeraciones.

@masak ¿Qué? ¡No, no es así! Dado que la enumeración extendida es un tipo más amplio y no se puede asignar a la enumeración original, siempre sabrá todos los valores de cada enumeración que utilice. Extender en este contexto significa crear una nueva enumeración, no modificar la anterior.

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, ¿entonces quiere decir que extends aquí en efecto tiene más de una semántica de _copia_ (de los valores de enumeración de A a B )? Entonces, sí, los interruptores salen bien.

Sin embargo, hay _alguna_ expectativa allí que todavía me parece rota. Como una forma de tratar de concretarlo: con las clases, extends _no_ transmite la semántica de copia — los campos y métodos no se copian en la subclase extendida; en cambio, solo están disponibles a través de la cadena de prototipos. Solo hay un campo o método en la superclase.

Debido a esto, si class B extends A , tenemos la garantía de que B se puede asignar a A , por lo que, por ejemplo let a: A = new B(); estaría perfectamente bien.

Pero con enumeraciones y extends , no podríamos hacer let a: A = B.b; , porque no existe tal garantía correspondiente. Que es lo que me parece extraño; extends aquí transmite un cierto conjunto de suposiciones sobre lo que se puede hacer, y no se cumplen con enumeraciones.

¿Entonces simplemente llamándolo expands o clones ? 🤷‍♂️
Desde la perspectiva de los usuarios, se siente extraño que algo tan básico no sea fácil de lograr.

Si la semántica razonable requiere una palabra clave completamente nueva (sin gran parte del estado de la técnica en otros idiomas), ¿por qué no reutilizar la sintaxis extendida ( ... ) como se sugiere en OP y este comentario ?

+1 Espero que esto se agregue a la función de enumeración predeterminada. :)

¿Alguien sabe alguna solución elegante??? 🧐

Si la semántica razonable requiere una palabra clave completamente nueva (sin mucho de la técnica anterior en otros idiomas), ¿por qué no reutilizar la sintaxis extendida (...) como se sugiere en OP y este comentario?

Sí, después de pensarlo un poco más, creo que esta solución sería buena.

Después de leer todo el hilo de este problema, parece que hay un amplio acuerdo de que reutilizar el operador de propagación resuelve el problema y aborda todas las preocupaciones que la gente ha planteado sobre hacer que la sintaxis sea confusa/poco intuitiva.

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

¿Este problema realmente todavía necesita la etiqueta "Esperando más comentarios" en este momento, @RyanCavanaugh ?

La característica buscada +1

¿Tenemos alguna noticia sobre este tema? Se siente realmente útil tener implementado el operador de propagación para las enumeraciones.

Especialmente para los casos de uso que implican la metaprogramación, la capacidad de crear alias y ampliar las enumeraciones se encuentra entre algo imprescindible y agradable. Actualmente, no hay forma de tomar una enumeración y export con otro nombre, a menos que recurra a una de las soluciones mencionadas anteriormente.

@m93a Ah, ¿entonces quiere decir que extends aquí en efecto tiene más de una semántica de _copia_ (de los valores de enumeración de A a B )? Entonces, sí, los interruptores salen bien.

Sin embargo, hay _alguna_ expectativa allí que todavía me parece rota. Como una forma de tratar de concretarlo: con las clases, extends _no_ transmite la semántica de copia — los campos y métodos no se copian en la subclase extendida; en cambio, solo están disponibles a través de la cadena de prototipos. Solo hay un campo o método en la superclase.

Debido a esto, si class B extends A , tenemos la garantía de que B se puede asignar a A , por lo que, por ejemplo let a: A = new B(); estaría perfectamente bien.

Pero con enumeraciones y extends , no podríamos hacer let a: A = B.b; , porque no existe tal garantía correspondiente. Que es lo que me parece extraño; extends aquí transmite un cierto conjunto de suposiciones sobre lo que se puede hacer, y no se cumplen con enumeraciones.

@masak Creo que estás cerca de la razón, pero hiciste una pequeña suposición que es incorrecta. B es asignable a A en el caso de enum B extends A como "asignable" significa que todos los valores proporcionados por A están disponibles en B. Cuando dijo que let a: A = B.b está asumiendo que los valores en B deben ser disponible en A, que no es lo mismo que los valores asignables a A. let a: A = B.a ES correcto porque B es asignable a A.

Esto es evidente usando clases como en el siguiente ejemplo:

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`

Vínculo de juegos de TypeScript

invalid access

Para resumir, creo que extiende ES la terminología correcta, ya que eso es exactamente lo que se está haciendo. En el ejemplo enum B extends A , SIEMPRE puede esperar que una enumeración B contenga todos los valores posibles de la enumeración A, porque B es una "subclase" (¿subenumeración? Tal vez haya una palabra mejor para esto) de A y, por lo tanto, asignable a A.

Así que no creo que necesitemos una nueva palabra clave, creo que deberíamos usar extends Y creo que esto debería ser una parte nativa de TypeScript: D

@julian-sf Creo que estoy de acuerdo con todo lo que escribiste...

...pero... :slightly_smiling_face:

como problematicé aquí , ¿qué pasa con esta situación?

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

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

Dado que Pause es una _instancia_ de AdvEvents y AdvEvents _extends_ BasicEvents , también esperaría que Pause fuera una _instancia_ de BasicEvents ? (Porque eso parece seguirse de cómo interactúan las relaciones instancia/herencia).

Por otro lado, la propuesta de valor central de las enumeraciones (en mi humilde opinión) es que son _cerradas_/"finales" (como en, no extensibles) para que algo como una declaración switch pueda asumir la totalidad. (Y entonces AdvEvents poder _extender_ lo que significa ser un BasicEvent viola algún tipo de Menos sorpresa para las enumeraciones).

No creo que puedas tener más de dos de las siguientes tres propiedades:

  • Enumeraciones cerradas/finales/previsiblemente totales
  • Una relación extends entre dos declaraciones enum
  • La suposición (razonable) de que si b es una instancia de B y B extends A , entonces b es una instancia de A

@masak Entiendo y estoy de acuerdo con el principio cerrado de las enumeraciones (en tiempo de ejecución). Pero la extensión del tiempo de compilación no violaría el principio cerrado en tiempo de ejecución, ya que el compilador los definiría y construiría.

La suposición (razonable) de que si b es una instancia de B y B extiende A, entonces b es una instancia de A

Creo que este razonamiento es un poco engañoso ya que la dicotomía instancia/clase no es realmente asignable a la enumeración. Las enumeraciones no son clases y no tienen instancias. Sin embargo, creo que pueden ser extensibles, si se hacen correctamente. Piense en enumeraciones más como conjuntos. En este ejemplo, B es un superconjunto de A. Por lo tanto, es razonable suponer que cualquier valor de A está presente en B, pero que solo ALGUNOS valores de B estarán presentes en A.

Entiendo de dónde viene la preocupación... aunque. Y no estoy seguro de qué hacer al respecto. Un buen ejemplo de un problema con la extensión de enumeración:

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

En este caso, las enumeraciones se comportan de manera bastante diferente a las clases. Si se tratara de clases, esperaría poder convertir B en A con bastante facilidad, pero eso claramente no funcionará aquí. No necesariamente creo que esto sea MALO, creo que debería tenerse en cuenta. IE, no puede abarcar un tipo de enumeración hacia arriba en su árbol de herencia como una clase. Esto podría explicarse con un error del compilador del tipo "no se puede asignar la enumeración de superconjunto B a A, ya que no todos los valores de B están presentes en A".

@julian-sf

Creo que este razonamiento es un poco engañoso ya que la dicotomía instancia/clase no es realmente asignable a la enumeración. Las enumeraciones no son clases y no tienen instancias.

Tienes toda la razón, a primera vista.

  • Visto como una construcción de lenguaje independiente, una enumeración tiene _miembros_, no instancias. El término "miembro" se utiliza tanto en el Manual como en la Especificación del idioma. (C# y Python usan de manera similar el término "miembro". Java usa "constante de enumeración", porque "miembro" tiene un significado sobrecargado en Java).
  • Visto desde una perspectiva de código compilado, una enumeración tiene _propiedades_, asignando ambos sentidos: nombres a valores y valores a nombres. De nuevo, no instancias.

Pensando en esto, me doy cuenta de que estoy un poco coloreado por la versión de Java de las enumeraciones. En Java, los valores de enumeración son literalmente instancias de su tipo de enumeración. En cuanto a la implementación, una enumeración es una clase que extiende la clase Enum . (No está permitido hacerlo manualmente , tiene que usar la palabra clave enum , pero eso es lo que sucede bajo el capó). Lo bueno de esto es que las enumeraciones obtienen todas las comodidades que hacen las clases: puede tener campos, constructores, métodos... En este enfoque, los miembros de enumeración _son_ instancias. (El JLS lo dice).

Tenga en cuenta que no estoy proponiendo ningún cambio en la semántica de enumeración de TypeScript. En particular, no digo que TypeScript deba cambiar para usar el modelo de Java para las enumeraciones. Estoy diciendo que es instructivo/esclarecedor superponer una "comprensión" de clase/instancia sobre enumeraciones/miembros de enumeración. No "una enumeración _es_ una clase" o "un miembro de enumeración _es_ una instancia"... pero hay similitudes que se trasladan.

¿Qué similitudes? En primer lugar, escriba la membresía.

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

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

La última línea no verifica el tipo, porque Bar.Z no es un Foo . De nuevo, no se trata _en realidad_ de clases e instancias, pero se puede _entender_ usando el mismo modelo, como si Foo y Bar fueran clases y los seis miembros fueran sus respectivas instancias.

(Ignoraremos a los efectos de este argumento el hecho de que let foo: Foo = 2; también es semánticamente legal y que, en general, los valores number son asignables a variables de tipo enumeración).

Las enumeraciones tienen la propiedad adicional de que están _cerradas_; lo siento, no conozco un término mejor para esto; una vez que las define, no puede extenderlas. Específicamente, los miembros enumerados dentro de la declaración de enumeración son las _únicas_ cosas que coinciden con el tipo de enumeración. ("Cerrado" como en "hipótesis de mundo cerrado".) Esta es una gran propiedad porque puede verificar con total certeza que todos los casos en una declaración switch en su enumeración han sido cubiertos.

Con extends en las enumeraciones, esta propiedad desaparece.

Usted escribe,

Entiendo y estoy de acuerdo con el principio cerrado de las enumeraciones (en tiempo de ejecución). Pero la extensión del tiempo de compilación no violaría el principio cerrado en tiempo de ejecución, ya que el compilador los definiría y construiría.

No creo que eso sea cierto, porque asume que cualquier código que extienda su enumeración está en su proyecto. Pero un módulo de terceros puede extender su enumeración y, de repente, hay _nuevos_ miembros de enumeración que también se pueden asignar a su enumeración, fuera del control del código que compila. Esencialmente, las enumeraciones ya no se cerrarían, ni siquiera en tiempo de compilación.

Todavía siento que soy algo torpe al expresar exactamente lo que quiero decir, pero creo que es importante: extends en enum rompería una de las características más preciadas de las enumeraciones, el hecho de que re cerrado Cuente cuántos idiomas absolutamente _prohiben_ extender/subclasificar una enumeración, por esta misma razón.

Pensé un poco más en las soluciones alternativas y, partiendo de #17592 (comentario) , puede hacerlo un poco mejor definiendo también Events en el espacio de valores:

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;

De mis pruebas, parece que Events.Start se interpreta correctamente como BasicEvents.Start en el sistema de tipos, por lo que la verificación exhaustiva y el refinamiento de unión discriminado parecen funcionar bien. Lo principal que falta es que no puede usar Events.Pause como un tipo literal; necesitas AdvEvents.Pause . Puede usar typeof Events.Pause y se resuelve en AdvEvents.Pause , aunque la gente de mi equipo se ha confundido con ese tipo de patrón y creo que en la práctica recomendaría AdvEvents.Pause al usar es como un tipo.

(Esto es para el caso cuando desea que los tipos de enumeración se puedan asignar entre sí en lugar de enumeraciones aisladas. Según mi experiencia, es más común querer que se puedan asignar).

Creo que esta es la mejor solución a la mano, en este momento.

Gracias @alangpierce :+1:

¿Algún avance en esto?

@sdwvit No soy una de las personas principales, pero desde mi punto de vista, la siguiente propuesta de sintaxis (de OP, pero re-sugerida dos veces después de eso) haría felices a todos, sin ningún problema conocido:

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

Me haría feliz, porque significaría implementar esta característica aparentemente útil de "duplicar todos los miembros en esta otra enumeración" _sin_ usar extends , que considero problemático por las razones que he mencionado. La sintaxis ... evita estos problemas copiando, no extendiendo.

El problema todavía está marcado como "Esperando más comentarios", y respeto el derecho de los miembros principales a mantenerlo en esa categoría durante el tiempo que consideren necesario. Pero también, no hay nada que impida que alguien implemente lo anterior y lo envíe como PR.

@masak gracias por la respuesta. Ahora tengo que revisar todo el historial de discusiones. Te responderé después :)

Me encantaría que esto sucediera y me encantaría intentar implementarlo yo mismo. Sin embargo, todavía necesitamos definir comportamientos para todas las enumeraciones. Todo esto funciona bien para las enumeraciones basadas en cadenas, pero ¿qué pasa con las enumeraciones numéricas estándar? ¿Cómo funciona extender/copiar aquí?

  • Supongo que solo querremos permitir extender una enumeración con una enumeración del "mismo tipo" (numérico extiende numérico, cadena extiende cadena). Las enumeraciones heterogéneas tienen soporte técnico, así que supongo que deberíamos mantener ese soporte.

  • ¿Deberíamos permitir extender desde múltiples enumeraciones? ¿Deberían tener todos valores mutuamente excluyentes? ¿O permitiremos valores superpuestos? ¿Prioridad basada en el orden léxico?

  • ¿Pueden las enumeraciones extendidas anular los valores de las enumeraciones extendidas?

  • ¿Deben aparecer las enumeraciones extendidas al comienzo de la lista de valores o pueden estar en cualquier orden? ¿Supongo que los valores definidos más tarde tienen mayor prioridad?

  • Supongo que los valores numéricos implícitos continuarán 1 después del valor máximo de las enumeraciones numéricas extendidas.

  • ¿Consideraciones especiales para las máscaras de bits?

etcétera etcétera.

@JeffreyMercado Estas son buenas preguntas y apropiadas para alguien que espera intentar una implementación. :sonreír:

A continuación se encuentran mis respuestas, guiadas por un enfoque de diseño "conservador" (como en "tomemos decisiones de diseño que rechacen los casos de los que no estamos seguros, en lugar de tomar decisiones ahora que son difíciles de cambiar más adelante mientras permanecemos compatibles con versiones anteriores").

  • Supongo que solo querremos permitir extender una enumeración con una enumeración del "mismo tipo" (numérico extiende numérico, cadena extiende cadena)

Yo también lo asumo. La enumeración resultante de tipo mixto no parece muy útil.

  • ¿Deberíamos permitir extender desde múltiples enumeraciones? ¿Deberían tener todos valores mutuamente excluyentes? ¿O permitiremos valores superpuestos? ¿Prioridad basada en el orden léxico?

Dado que estamos hablando de copiar la semántica, duplicar varias enumeraciones parece "más correcto" que la herencia múltiple a la C++. No veo ningún problema de inmediato, especialmente si seguimos construyendo sobre la analogía de la distribución de objetos: let newEnum = { ...enumA, ...enumB };

¿Todos los miembros deben tener valores mutuamente excluyentes? Lo conservador sería decir "sí". Una vez más, la analogía de la dispersión de objetos nos proporciona una semántica alternativa: el último gana.

No puedo pensar en ningún caso de uso en el que apreciaría poder anular los valores de enumeración. Pero eso podría ser sólo una falta de imaginación de mi parte. El enfoque conservador de no permitir colisiones tiene las agradables propiedades de que es fácil de explicar/internalizar y, al menos en teoría, podría exponer errores de diseño reales (en código nuevo o en código que se está manteniendo).

  • ¿Pueden las enumeraciones extendidas anular los valores de las enumeraciones extendidas?

Creo que la respuesta y el razonamiento son muy parecidos en este caso que en el caso anterior.

  • ¿Deben aparecer las enumeraciones extendidas al comienzo de la lista de valores o pueden estar en cualquier orden? ¿Supongo que los valores definidos más tarde tienen mayor prioridad?

Iba a decir primero que esto solo importa si vamos con la semántica de anulación "el último gana".

Pero pensándolo bien, tanto en "sin colisiones" como en "el último gana", me resulta extraño incluso _querer_ poner declaraciones de miembros de enumeración antes de que la enumeración se extienda en la lista. Como, ¿qué intención se está comunicando al hacerlo? Los diferenciales son un poco como "importaciones", y estos van convencionalmente en la parte superior.

No necesariamente quiero prohibir poner pliegos de enumeración después de las declaraciones de miembros de enumeración (aunque creo que estaría bien si no se permite en la gramática). Si termina siendo permitido, definitivamente es algo que los linters y la convención de la comunidad podrían señalar como evitable. Simplemente no hay una razón de peso para hacerlo.

  • Supongo que los valores numéricos implícitos continuarán 1 después del valor máximo de las enumeraciones numéricas extendidas.

Tal vez lo conservador sea requerir un valor explícito para el primer miembro después de una extensión.

  • ¿Consideraciones especiales para las máscaras de bits?

Creo que eso estaría cubierto por la regla anterior.

Pude hacer algo razonable al combinar enumeraciones, interfaces y objetos inmutables.

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 Ese es un buen uso de las enumeraciones, pero... No veo cómo cuenta como extender una enumeración existente. Lo que estás haciendo es _usar_ la enumeración.

(Además, a diferencia de las declaraciones de enumeración y cambio, parece que en su ejemplo no tiene verificación de totalidad; alguien que agregó un miembro de enumeración más tarde podría olvidarse fácilmente de agregar una clave correspondiente en SINGULAR y PLURAL registro en todas las instancias de Label .)

@masak

alguien que agregó un miembro de enumeración más tarde podría olvidarse fácilmente de agregar una clave correspondiente en el registro SINGULAR y PLURAL en todas las instancias de Label ).

Al menos en mi entorno, arroja un error cuando falta un miembro de enumeración de SINGULAR o PLURAL . El tipo Record hace su trabajo, supongo.

Si bien la documentación para TS es buena, creo que no hay muchos ejemplos de cómo combinar muchas funciones de una manera no trivial. enum inheritance fue lo primero que busqué cuando traté de resolver problemas de internacionalización, lo que me llevó a este hilo. De todos modos, el enfoque resultó ser incorrecto, por lo que escribí esta publicación.

@illeatmyhat

Al menos en mi entorno, arroja un error cuando falta un miembro de enumeración de SINGULAR o PLURAL . El tipo Record hace su trabajo, supongo.

¡Oh! TIL. Y sí, eso lo hace mucho más interesante. Veo lo que quiere decir acerca de alcanzar inicialmente la herencia de enumeración y finalmente aterrizar en su patrón. Puede que eso ni siquiera sea algo aislado; Los "problemas X/Y" son algo real. Más personas podrían comenzar con el pensamiento "Quiero extender MyEnum ", pero terminar usando Record<MyEnum, string> como lo hizo usted.

Responder a @masak :

Con extensiones en enumeraciones, esta propiedad desaparece.

Usted escribe,

@ julian-sf: entiendo y estoy de acuerdo con el principio cerrado de las enumeraciones (en tiempo de ejecución). Pero la extensión del tiempo de compilación no violaría el principio cerrado en tiempo de ejecución, ya que el compilador los definiría y construiría.

No creo que eso sea cierto, porque asume que cualquier código que extienda su enumeración está en su proyecto. Pero un módulo de terceros puede extender su enumeración y, de repente, hay nuevos miembros de enumeración que también se pueden asignar a su enumeración, fuera del control del código que compila. Esencialmente, las enumeraciones ya no se cerrarían, ni siquiera en tiempo de compilación.

Cuanto más pienso en esto, tienes toda la razón. Las enumeraciones deben estar cerradas. Realmente me gusta la idea de "componer" enumeraciones, ya que creo que este es realmente el meollo del asunto que queremos aquí 🥳.

Creo que esta notación resume el concepto de "unir" dos enumeraciones separadas con bastante elegancia:

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

Entonces considere que mi renuncia al uso del término extends 😆


Comentarios sobre las respuestas de @masak a las preguntas de @JeffreyMercado :

  • Supongo que solo querremos permitir extender una enumeración con una enumeración del "mismo tipo" (numérico extiende numérico, cadena extiende cadena). Las enumeraciones heterogéneas tienen soporte técnico, así que supongo que deberíamos mantener ese soporte.

Yo también lo asumo. La enumeración resultante de tipo mixto no parece muy útil.

Si bien estoy de acuerdo en que no es útil, probablemente DEBEMOS mantener un soporte heterogéneo para las enumeraciones aquí. Creo que una advertencia de linter sería útil aquí, pero no creo que TS deba interponerse en eso. Puedo pensar en un caso de uso artificial que es, estoy creando una enumeración para interacciones con una API muy mal diseñada que toma indicadores que son una combinación de números y cadenas. Artificioso, lo sé, pero dado que está permitido en otros lugares, no creo que debamos prohibirlo aquí.

¿Quizás solo un fuerte estímulo para no hacerlo?

  • ¿Deberíamos permitir extender desde múltiples enumeraciones? ¿Deberían tener todos valores mutuamente excluyentes? ¿O permitiremos valores superpuestos? ¿Prioridad basada en el orden léxico?

Dado que estamos hablando de copiar la semántica, duplicar varias enumeraciones parece "más correcto" que la herencia múltiple a la C++. No veo ningún problema de inmediato, especialmente si seguimos construyendo sobre la analogía de la propagación de objetos: let newEnum = { ...enumA, ...enumB };

100% de acuerdo

  • ¿Todos los miembros deben tener valores mutuamente excluyentes?

Lo conservador sería decir "sí". Una vez más, la analogía de la dispersión de objetos nos proporciona una semántica alternativa: el último gana.

Estoy destrozado aquí. Si bien estoy de acuerdo en que es una "mejor práctica" hacer cumplir la exclusividad mutua de los valores, ¿es correcto? Es directamente contradictorio con la semántica extendida comúnmente conocida. Por un lado, me gusta la idea de imponer valores mutuamente excluyentes, por otro lado, rompe muchas suposiciones sobre cómo debería funcionar la semántica extendida. ¿Hay algún inconveniente en seguir las reglas normales de distribución con "el último gana"? Parece que es más fácil de implementar (ya que el objeto subyacente es solo un mapa de todos modos). Pero también parece alinearse con las expectativas comunes. Me inclino por ser menos sorprendente.

También puede haber buenos ejemplos para querer anular un valor (aunque no tengo idea de cuáles serían).

Pero pensándolo bien, tanto en "sin colisiones" como en "el último gana", me resulta extraño incluso querer poner declaraciones de miembros de enumeración antes de que la enumeración se extienda en la lista. Como, ¿qué intención se está comunicando al hacerlo? Los diferenciales son un poco como "importaciones", y estos van convencionalmente en la parte superior.

Bueno, eso depende, si estamos siguiendo la semántica extendida, entonces no debería importar cuál sea el orden. Honestamente, incluso si estamos aplicando valores mutuamente excluyentes, el orden realmente no importaría, ¿verdad? Una colisión sería un error en ese punto, independientemente del orden.

  • Supongo que los valores numéricos implícitos continuarán 1 después del valor máximo de las enumeraciones numéricas extendidas.

Tal vez lo conservador sea requerir un valor explícito para el primer miembro después de una extensión.

Estoy de acuerdo. Si distribuye una enumeración, TS solo debería aplicar valores explícitos para miembros adicionales.

@julian-sf

Así que considera que se extiende mi renuncia al uso del término 😆

:+1: La Sociedad para la Preservación de los Tipos Suma aplaude desde el banquillo.

Pero pensándolo bien, tanto en "sin colisiones" como en "el último gana", me resulta extraño incluso querer poner declaraciones de miembros de enumeración antes de que la enumeración se extienda en la lista. Como, ¿qué intención se está comunicando al hacerlo? Los diferenciales son un poco como "importaciones", y estos van convencionalmente en la parte superior.

Bueno, eso depende, si estamos siguiendo la semántica extendida, entonces no debería importar cuál sea el orden. Honestamente, incluso si estamos aplicando valores mutuamente excluyentes, el orden realmente no importaría, ¿verdad? Una colisión sería un error en ese punto, independientemente del orden.

Estoy diciendo "no hay una buena razón para colocar diferenciales después de las declaraciones de miembros normales"; estás diciendo "bajo las restricciones apropiadas, colocarlos antes o después no hace ninguna diferencia". Ambas cosas pueden ser ciertas al mismo tiempo.

La principal diferencia en el resultado parece caer en un espectro de permitir o no permitir diferenciales antes de los miembros normales. Podría estar sintácticamente deshabilitado; podría producir una advertencia de pelusa; o podría estar completamente bien en todos los aspectos. Si el orden no hace ninguna diferencia semántica, entonces todo se reduce a hacer que la función de distribución de enumeración siga el principio de Least Surprise , fácil de usar y fácil de enseñar/explicar.

El uso del operador de extensión cae en el uso más amplio de copia superficial en JS y TypeScript. Sin duda, es el método más utilizado y más fácil de entender que usar extends , lo que implica una relación directa. Crear una enumeración a través de la composición sería la solución más fácil de consumir.

Algunas de las sugerencias de trabajo en torno, si bien son válidas y utilizables, agregan mucho más código repetitivo para lograr el mismo resultado deseado. Dada la naturaleza final e inmutable de una enumeración, sería deseable crear enumeraciones adicionales a través de la composición, para mantener las propiedades que son consistentes con otros lenguajes.

Es una pena que aún transcurran 3 años de esta conversación.

@ jmitchell38488 Le daría un me gusta a tu comentario, pero tu última oración me hizo cambiar de opinión. Esta es una discusión necesaria, ya que la solución propuesta funcionaría, pero también implica la posibilidad de extender clases e interfaces de esta manera. Es un gran cambio que puede asustar a algunos programadores de lenguajes tipo C++ de usar mecanografiado, ya que básicamente terminas con 2 formas de hacer lo mismo ( class A extends B y class A { ...(class B {}) } ). Creo que se pueden admitir ambas formas, pero luego necesitamos extend para las enumeraciones también para mantener la coherencia.

@masak ¿qué pasa? ^

@sdwvit No me refiero a cambiar el comportamiento para crear clases e interfaces, estoy hablando específicamente de enumeraciones y crearlas a través de la composición. Son tipos finales inmutables, por lo que no deberíamos poder extendernos de la forma típica de herencia.

Dada la naturaleza de JS y el valor transpilado final, no hay razón por la que no se pueda lograr la composición. Seguro que haría que trabajar con enumeraciones fuera más atractivo.

@masak ¿qué pasa? ^

Hm. Creo que la consistencia del lenguaje es un objetivo loable y, por lo tanto, no es _a priori_ incorrecto solicitar una característica similar ... en clases e interfaces. Pero creo que el caso es más débil o inexistente allí, y por dos razones: (a) las clases y las interfaces ya tienen un mecanismo de extensión, y agregar un segundo proporciona poco valor adicional (mientras que proporcionar uno para enumeraciones cubriría un caso de uso que la gente han estado volviendo a este tema durante años); (b) agregar nueva sintaxis y semántica a las clases tiene una barra mucho más alta para aprobación, ya que las clases son, en cierto sentido, de la especificación EcmaScript. (TypeScript es más antiguo que ES6, pero ha habido un esfuerzo activo para que el primero se mantenga cerca del último. Eso incluye no introducir funciones adicionales en la parte superior).

Creo que este hilo ha estado abierto durante mucho tiempo simplemente porque representa una característica valiosa que cubriría casos de uso reales, pero aún no se ha hecho una RP para ello. Hacer una RP de este tipo requiere tiempo y esfuerzo, más que solo decir que quieres la función. :guiño:

¿Alguien está trabajando en esta característica?

¿Alguien está trabajando en esta característica?

Supongo que no, ya que ni siquiera hemos terminado la discusión al respecto, ¡jaja!

Siento que nos hemos acercado a un consenso sobre cómo sería esto. Dado que esto sería una adición de idioma, probablemente requeriría un "campeón" para impulsar esta propuesta. Creo que alguien del equipo de TS tendría que venir y mover el problema de "En espera de comentarios" a "En espera de propuesta" (o algo similar).

Estoy trabajando en un prototipo de ello. Aunque no he llegado muy lejos por falta de tiempo y falta de familiaridad con la estructura del código. Quiero ver esto y si no hay otro movimiento, continuaré cuando pueda.

También me encantaría esta característica. Han pasado 37 meses, esperemos que se avance pronto

Notas de la reunión reciente:

  • Nos gusta la sintaxis extendida, porque extends implica un subtipo, mientras que "extender" una enumeración crea un supertipo.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • La idea es que AdvEvents.Start se resolvería con el mismo tipo de identidad que BasicEvents.Start . Esto tiene las implicaciones de que los tipos BasicEvents.Start y AdvEvents.Start serían asignables entre sí, y el tipo BasicEvents sería asignable a AdvEvents . Con suerte, esto tiene sentido intuitivo, pero es importante tener en cuenta que esto significa que la extensión no es solo un atajo sintáctico, si expandimos la extensión en lo que parece que significa:

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

    esto tiene un comportamiento diferente: aquí, BasicEvents.Start y AdvEvents.Start _no_ son asignables entre sí, debido a la calidad opaca de las enumeraciones de cadenas.

    Otra consecuencia menor de esta implementación es que AdvEvents.Start probablemente se serializaría como BasicEvents.Start en información rápida y emisión de declaración (al menos donde no hay contexto sintáctico para vincular el miembro a AdvEvents —pasar el cursor sobre la expresión literal AdvEvents.Start podría dar como resultado una información rápida que dice AdvEvents.Start , pero podría ser más claro mostrar BasicEvents.Start todos modos).

  • Esto solo se permitiría en enumeraciones de cadena.

Me gustaría probarme este.

Para aclarar: Esto no está aprobado para su implementación.

Hay dos comportamientos posibles aquí, y ambos parecen malos.

Opción 1: en realidad es azúcar

Si la extensión realmente significa lo mismo que copiar los miembros de la enumeración extendida, entonces habrá una gran sorpresa cuando las personas intenten usar el valor de la enumeración extendida como si fuera el valor de la enumeración extendida y no funciona.

Opción 2: en realidad no es azúcar

La opción preferida sería que la extensión funcione más como la sugerencia de tipo de unión de @aj-r cerca de la parte superior del hilo. Si ese es el comportamiento que la gente ya quiere, entonces las opciones existentes sobre la mesa parecen estrictamente preferibles en aras de la comprensión. De lo contrario, estamos creando un nuevo tipo de enumeración de cadena que no se comporta como cualquier otra enumeración de cadena, lo cual es extraño y parece socavar la "simple" de la sugerencia aquí.

¿Qué comportamiento quiere la gente y por qué?

No quiero la opción 1, porque lleva a grandes sorpresas.

Quiero la opción 2, pero me encantaría tener suficiente soporte de sintaxis para superar la desventaja que mencionó @aj-r, para poder escribir let e: Events = Events.Pause; a partir de su ejemplo. La desventaja no es terrible, pero es un lugar donde la enumeración extendida no puede ocultar la implementación; así que es un poco asqueroso.

También creo que la opción 1 es una mala idea. Busqué referencias a este problema en mi empresa y encontré dos revisiones de código donde estaba vinculado, y en ambos casos (y en mi experiencia personal), existe una clara necesidad de que los elementos de la enumeración más pequeña se puedan asignar al tipo de enumeración más grande. . Me preocupa especialmente que una persona presente ... pensando que se comporta como la opción 2, y luego la siguiente persona se confunda realmente (o recurra a trucos como as unknown as Events.Pause ) cuando los casos más complejos no funcionan.

Ya hay muchas formas de tratar de obtener el comportamiento de la opción 2: muchos fragmentos en este hilo, además de varios enfoques que involucran uniones de cadenas literales. Mi gran preocupación con la implementación de la opción 1 es que efectivamente presenta otra forma incorrecta de obtener la opción 2 y, por lo tanto, conduce a una mayor resolución de problemas y frustración para las personas que aprenden esta parte de TypeScript.

¿Qué comportamiento quiere la gente y por qué?

Dado que el código habla más que las palabras, y usando el ejemplo del 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

Este ejemplo muestra la opción 2, ¿verdad? Si es así, entonces quiero desesperadamente la opción 2.

De lo contrario, estamos creando un nuevo tipo de enumeración de cadena que no se comporta como cualquier otra enumeración de cadena, lo cual es extraño y parece socavar la "simple" de la sugerencia aquí.

Estoy de acuerdo en que la opción 2 es un poco inquietante y puede ser mejor dar un paso atrás y pensar en alternativas. Aquí hay una exploración de cómo se podría hacer sin agregar ninguna sintaxis o comportamiento nuevo a las enumeraciones de hoy:

Creo que mi sugerencia en https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 se acerca más estos días y podría ser algo con lo que trabajar:

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

Veo dos problemas principales con ese enfoque:

  • Es realmente confuso para alguien que está aprendiendo TypeScript; si no tiene una comprensión sólida del espacio de tipos frente al espacio de valores, parece que está sobrescribiendo un tipo con una constante.
  • Está incompleto ya que no le permite usar Events.Pause como tipo.

Anteriormente sugerí https://github.com/microsoft/TypeScript/issues/29130 que (entre otras cosas) abordaría el segundo punto, y creo que aún podría ser valioso, aunque ciertamente agregaría mucha sutileza a cómo funcionan los nombres.

Una idea de sintaxis que creo que abordaría ambos puntos es una sintaxis alternativa const que también declara un tipo:

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

Esto se acercaría más a un mundo donde las enumeraciones no son necesarias en absoluto. (Para mí, las enumeraciones siempre me han parecido que van en contra de los objetivos de diseño de TS, ya que son una sintaxis de nivel de expresión con un comportamiento de emisión no trivial y nominal de forma predeterminada). La sintaxis de la declaración const type también le permitiría cree una enumeración de cadena de tipo estructural como esta:

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

Es cierto que la forma en que esto debería funcionar es que el tipo BasicEvents es una abreviatura de typeof BasicEvents[keyof typeof BasicEvents] , que podría estar demasiado enfocado para una sintaxis con nombre genérico como const type . Tal vez una palabra clave diferente sería mejor. Lástima que const enum ya está tomado 😛.

A partir de ahí, creo que la única brecha entre las enumeraciones (de cadena) y los literales de objetos sería la tipificación nominal. Eso posiblemente podría resolverse usando una sintaxis como as unique o const unique type que básicamente opta por un comportamiento de escritura nominal similar a una enumeración para estos tipos de objetos, e idealmente también para constantes de cadena regulares. Eso también daría una opción clara entre la opción 1 y la opción 2 al definir Events : usa unique cuando pretende que Events sea un tipo completamente distinto (opción 1), y omite unique cuando desea la asignabilidad entre Events y BasicEvents (opción 2).

Con const type y const unique type , habría una forma de unir enumeraciones existentes de manera limpia, y también una forma de expresar enumeraciones como una combinación natural de características de TS en lugar de una única.

¿Que esta pasando aqui? 😅

guau del 2017 🤪, ¿qué más comentarios necesitas?

¿Qué más comentarios necesitas?

¿¿¿Aquí mismo??? ¡No solo implementamos sugerencias porque son antiguas!

¿Qué más comentarios necesitas?

¿¿¿Aquí mismo??? ¡No solo implementamos sugerencias porque son antiguas!

Si. No estaba seguro de qué manera era.

Leyendo de nuevo y viendo también #40998, creo que es la mejor manera... los emuns son objetos y creo que es más fácil fusionar/extender enumeraciones.

No creo que esté lo suficientemente calificado para ofrecer mi opinión sobre el diseño de lenguajes, pero creo que puedo dar retroalimentación como desarrollador habitual.

Me encontré con este problema un par de semanas antes en un proyecto real en el que quería usar esta función. Terminé usando el enfoque de @alangpierce . Desafortunadamente, debido a mis responsabilidades con mi empleador, no puedo compartir el código aquí, pero aquí hay algunos puntos:

  1. Repetir declaraciones (tanto type como const para una nueva enumeración) no fue un gran problema y no dañó mucho la legibilidad.
  2. En mi caso, enum representaba diferentes acciones para un determinado algoritmo y había diferentes conjuntos de acciones para diferentes situaciones en este algoritmo. El uso de la jerarquía de tipos me permitió verificar que ciertas acciones no podían ocurrir en tiempo de compilación: este era el objetivo de todo y resultó ser bastante útil.
  3. El código que escribí era una biblioteca interna, y aunque la distinción entre diferentes tipos de enumeración era importante dentro de la biblioteca, no importaba a los usuarios externos de esta biblioteca. Con este enfoque, pude exportar solo un tipo que era el tipo de suma de todas las enumeraciones diferentes en el interior y ocultar los detalles de implementación.
  4. Desafortunadamente, no pude encontrar una forma idiomática y fácil de leer para analizar los valores del tipo de suma automáticamente a partir de las declaraciones de tipo. (En mi caso, los diferentes pasos del algoritmo que mencioné se cargaron desde la base de datos SQL en tiempo de ejecución). No fue un gran problema (escribir el código de análisis manualmente fue lo suficientemente sencillo), pero sería bueno si la implementación de la herencia de enumeración prestara atención a este problema.

En general, creo que muchos proyectos reales con una lógica empresarial aburrida se beneficiarían mucho de esta característica. Dividir enumeraciones en diferentes subtipos permitiría que el sistema de tipos verifique muchos invariantes que ahora son verificados por pruebas unitarias, y hacer que los valores incorrectos no se puedan representar por un sistema de tipos siempre es algo bueno.

Hola,

Déjame agregar mis dos centavos aquí 🙂

mi contexto

Tengo una API, con la documentación de OpenApi generada con tsoa .

Uno de mis modelos tiene un estado definido así:

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

Tengo un método setStatus que toma un subconjunto de estos estados. Como la función no está disponible, consideré duplicar la enumeración de esa manera:

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

Así que mi método se describe de esta manera:

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

con ese codigo me sale este error:

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.

lo cual haré por ahora, pero tenía curiosidad y comencé a buscar en este repositorio, cuando encontré esto.
Después de desplazarme hacia abajo, no encontré a nadie (o tal vez me perdí algo) que sugiera este caso de uso.

En mi caso de uso, no quiero "extender" desde una enumeración porque no hay razón para que EntityStatus sea una extensión de RequestedEntityStatus. Preferiría poder "Elegir" de la enumeración más genérica.

Mi propuesta

Encontré el operador de propagación mejor que la propuesta de extensión, pero me gustaría ir más allá y sugerir lo siguiente:

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

De manera más general, esto debería funcionar:

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

En otras palabras

Me gustaría relajar las restricciones sobre los tipos de enumeración para que se comporten más como uniones ( 'a' | 'b' ) que como estructuras opacas.

Al agregar esas habilidades al compilador, dos enumeraciones independientes con los mismos valores se pueden asignar entre sí con las mismas reglas que las uniones:
Dadas las siguientes enumeraciones:

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

Y tres variables a:A , b:B y c:C

  • c = a debería funcionar, porque A es solo un subconjunto de C, por lo que cada valor de A es un valor válido de C
  • c = b debería funcionar, ya que B es solo 'a' | 'c' , ambos valores válidos de C
  • b = a podría funcionar si se sabe que a es diferente de 'b' (que sería igual al tipo 'a' únicamente)
  • a = b , del mismo modo, podría funcionar si se sabe que b es diferente de 'c'
  • b = c podría funcionar si se sabe que c es diferente de 'b' (que equivaldría a 'a'|'c' , que es exactamente lo que es B)

¿O tal vez deberíamos necesitar un reparto explícito en los lados derechos, en cuanto a la comparación de igualdad?

Acerca de los conflictos de los miembros de la enumeración

No soy fanático de la regla de las "últimas ganancias", incluso si se siente natural con el operador de propagación.
Yo diría que el compilador debería devolver un error si se duplica una "clave" o un "valor" de la enumeración, a menos que tanto la clave como el valor sean idénticos:

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'

Clausura

Encuentro esta propuesta bastante flexible y natural para que trabajen los desarrolladores de TS, al tiempo que permite una mayor seguridad de tipo (en comparación con as unknown as T ) sin cambiar realmente lo que es una enumeración. Simplemente presenta una nueva forma de agregar miembros a la enumeración y una nueva forma de comparar enumeraciones entre sí.
¿Qué piensas? ¿Me perdí algún problema obvio de arquitectura del lenguaje que hace que esta función sea inalcanzable?

¿Fue útil esta página
0 / 5 - 0 calificaciones