Typescript: Estendendo enumerações baseadas em string

Criado em 3 ago. 2017  ·  68Comentários  ·  Fonte: microsoft/TypeScript

Antes das enumerações baseadas em string, muitos retornariam aos objetos. O uso de objetos também permite a extensão de tipos. Por exemplo:

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

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

Ao alternar para enums de string, é impossível conseguir isso sem redefinir o enum.

Seria muito útil poder fazer algo assim:

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

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

Considerando que os enums produzidos são objetos, isso também não será muito horrível:

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

Comentários muito úteis

Todas as soluções alternativas são boas, mas eu gostaria de ver o suporte à herança enum do próprio typescript para que eu possa usar verificações exaustivas o mais simples possível.

Todos 68 comentários

Apenas brinquei um pouco com isso e atualmente é possível fazer essa extensão usando um objeto para o tipo estendido, então isso deve funcionar bem:

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

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

Observe que você pode se aproximar

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

Outra opção, dependendo de suas necessidades, é usar um tipo de união:

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

let e: Events = AdvEvents.Pause;

A desvantagem é que você não pode usar Events.Pause ; você tem que usar AdvEvents.Pause . Se você estiver usando const enums, provavelmente está tudo bem. Caso contrário, pode não ser suficiente para o seu caso de uso.

Precisamos desse recurso para redutores Redux fortemente tipados. Por favor, adicione-o no TypeScript.

Outra solução é não usar enums, mas usar algo que se pareça com um 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];

Todas as soluções alternativas são boas, mas eu gostaria de ver o suporte à herança enum do próprio typescript para que eu possa usar verificações exaustivas o mais simples possível.

Basta usar class em vez de enum.

Eu estava apenas tentando isso.

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

Tem que haver uma maneira melhor de fazer isso.

Por que isso ainda não é um recurso? Sem mudanças de última hora, comportamento intuitivo, mais de 80 pessoas que procuraram ativamente e exigiram esse recurso - parece um acéfalo.

Mesmo reexportar enum de um arquivo diferente em um namespace é realmente estranho sem estender enums (e é impossível reexportar o enum de uma maneira que ainda seja enum e não objeto e 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
Atualmente usando uma solução alternativa, mas isso deve ser um recurso de enumeração nativo.

Eu dei uma olhada nesse problema para ver se alguém fez a seguinte pergunta. (Parecem não.)

Do OP:

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

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

As pessoas esperariam que AdvEvents fosse atribuído a BasicEvents ? (Como é, por exemplo, o caso de extends para classes.)

Se sim, então quão bem isso combina com o fato de que os tipos enum devem ser finais e não possíveis de estender?

@masak ótimo ponto. O recurso que as pessoas querem aqui definitivamente não é o normal extends . BasicEvents deve ser atribuído a AdvEvents , não o contrário. O extends normal refina outro tipo para ser mais específico e, neste caso, queremos ampliar o outro tipo para adicionar mais valores, portanto, qualquer sintaxe personalizada para isso provavelmente não deve usar a palavra-chave extends , ou pelo menos não use a sintaxe enum A extends B { .

Nessa nota, gostei da sugestão de propagação para isso do OP.

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

Porque a divulgação já traz a expectativa de que o original seja clonado superficialmente em uma cópia desconectada.

BasicEvents deve ser atribuído a AdvEvents , não o contrário.

Posso ver como isso pode ser verdade em todos os casos, mas não tenho certeza se deve ser verdade em todos os casos, se você entende o que quero dizer. Parece que depende do domínio e depende do motivo pelo qual esses valores enum foram copiados.

Pensei um pouco mais em soluções alternativas e trabalhando com https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 , você pode fazer um pouco melhor definindo também Events no valor espaço:

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;

Dos meus testes, parece que Events.Start é interpretado corretamente como BasicEvents.Start no sistema de tipos, portanto, a verificação de exaustividade e o refinamento discriminado da união parecem funcionar bem. A principal coisa que falta é que você não pode usar Events.Pause como um tipo literal; você precisa AdvEvents.Pause . Você pode usar typeof Events.Pause e resolve para AdvEvents.Pause , embora as pessoas da minha equipe tenham ficado confusas com esse tipo de padrão e eu acho que na prática eu incentivaria AdvEvents.Pause ao usar isso como um tipo.

(Isso é para o caso em que você deseja que os tipos de enumeração sejam atribuíveis entre si em vez de enums isolados. Pela minha experiência, é mais comum desejar que eles sejam atribuíveis.)

Outra sugestão (mesmo que não resolva o problema original), que tal usar literais de string para criar uma união de tipos?

type BEs = "Start" | "Finish";

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

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

Então, a solução para nossos problemas poderia ser essa?

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

Estender enums deve ser um recurso central do TypeScript. Apenas dizendo'

@wottpal Repetindo minha pergunta anterior:

Se [enums podem ser estendidos], então quão bem isso combina com o fato de que os tipos enum devem ser finais e não são possíveis de estender?

Especificamente, parece-me que a verificação de totalidade de uma instrução switch sobre um valor de enum depende da não extensibilidade de enums.

@masak O quê? Não, não! Como o enum estendido é um tipo mais amplo e não pode ser atribuído ao enum original, você sempre sabe todos os valores de cada enum que usa. Estender neste contexto significa criar um novo enum, não modificar o antigo.

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, então você quer dizer que extends aqui em vigor tem mais uma semântica de _cópia_ (dos valores enum de A em B )? Então, sim, os interruptores saem OK.

No entanto, há _alguma_ expectativa lá que ainda parece quebrada para mim. Como uma maneira de tentar acertar: com classes, extends _não_ transmite semântica de cópia — campos e métodos não são copiados para a subclasse de extensão; em vez disso, eles são apenas disponibilizados por meio da cadeia de protótipos. Existe apenas um campo ou método na superclasse.

Por causa disso, se class B extends A , temos a garantia de que B é atribuível a A , e assim, por exemplo let a: A = new B(); seria perfeitamente aceitável.

Mas com enums e extends , não poderíamos fazer let a: A = B.b; , porque não existe tal garantia correspondente. Que é o que me parece estranho; extends aqui transmite um certo conjunto de suposições sobre o que pode ser feito, e elas não são atendidas com enumerações.

Então é só chamar de expands ou clones ? 🤷‍♂️
Do ponto de vista dos usuários, parece estranho que algo tão básico não seja fácil de alcançar.

Se a semântica razoável requer uma palavra-chave totalmente nova (sem muito da técnica anterior em outros idiomas), por que não reutilizar a sintaxe de propagação ( ... ) conforme sugerido no OP e neste comentário ?

+1 Espero que isso seja adicionado ao recurso de enumeração padrão. :)

Alguém conhece alguma solução elegante??? 🧐

Se a semântica razoável requer uma palavra-chave totalmente nova (sem muito da técnica anterior em outras linguagens), por que não reutilizar a sintaxe de propagação (...) como sugerido no OP e neste comentário?

Sim, depois de pensar um pouco mais, acho que essa solução seria boa.

Depois de ler todo este tópico, parece que há um amplo acordo de que a reutilização do operador spread resolve o problema e aborda todas as preocupações que as pessoas levantaram sobre tornar a sintaxe confusa/não intuitiva.

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

Esse problema ainda precisa do rótulo "Aguardando mais feedback" neste momento, @RyanCavanaugh ?

O recurso queria +1

Temos novidades sobre este assunto? Parece realmente útil ter o operador de espalhamento implementado para enums.

Especialmente para casos de uso que envolvem metaprogramação, a capacidade de alias e estender enums está em algum lugar entre um must-have e um nice-to-have. Atualmente, não há como pegar um enum e export em outro nome - a menos que você recorra a uma das soluções alternativas mencionadas acima.

@ m93a Ah, então você quer dizer que extends aqui em vigor tem mais uma semântica de _cópia_ (dos valores enum de A em B )? Então, sim, os interruptores saem OK.

No entanto, há _alguma_ expectativa lá que ainda parece quebrada para mim. Como uma maneira de tentar acertar: com classes, extends _não_ transmite semântica de cópia — campos e métodos não são copiados para a subclasse de extensão; em vez disso, eles são apenas disponibilizados por meio da cadeia de protótipos. Existe apenas um campo ou método na superclasse.

Por causa disso, se class B extends A , temos a garantia de que B é atribuível a A , e assim, por exemplo let a: A = new B(); seria perfeitamente aceitável.

Mas com enums e extends , não poderíamos fazer let a: A = B.b; , porque não existe tal garantia correspondente. Que é o que me parece estranho; extends aqui transmite um certo conjunto de suposições sobre o que pode ser feito, e elas não são atendidas com enumerações.

@masak Acho que você está perto do correto, mas fez uma pequena suposição incorreta. B é atribuível a A no caso de enum B extends A como "atribuível" significa que todos os valores fornecidos por A estão disponíveis em B. Quando você disse que let a: A = B.b você está assumindo que os valores em B devem ser disponível em A, que não é o mesmo que os valores atribuíveis a A. let a: A = B.a ESTÁ correto porque B é atribuível a A.

Isso é evidente usando classes como no exemplo a seguir:

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`

Link do Playground TypeScript

invalid access

Para encurtar a história, acredito que estende a terminologia correta, pois é exatamente isso que está sendo feito. No exemplo enum B extends A você SEMPRE pode esperar que um enum B contenha todos os valores possíveis do enum A, porque B é uma "subclasse" (subenum? talvez haja uma palavra melhor para isso) de A e, portanto, atribuível a A.

Então eu não acho que precisamos de uma nova palavra-chave, acho que devemos usar extends E acho que isso deveria ser uma parte nativa do TypeScript :D

@julian-sf Acho que concordo com tudo que você escreveu...

...mas... :slightly_smiling_face:

como problematizei aqui , e essa situação?

// 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 é uma _instance_ de AdvEvents e AdvEvents _extends_ BasicEvents , você também esperaria que Pause fosse uma _instance_ de BasicEvents ? (Porque isso parece resultar de como as relações instância/herança geralmente interagem.)

Por outro lado, a proposta de valor central dos enums (IMHO) é que eles são _closed_/"final" (como em, não extensíveis) para que algo como uma instrução switch possa assumir a totalidade. (E então AdvEvents ser capaz de _estender_ o que significa ser um BasicEvent viola algum tipo de menor surpresa para enums.)

Eu não acho que você pode mais do que duas das três propriedades a seguir:

  • Enums sendo fechado/final/total previsível
  • Uma relação extends entre duas declarações enum
  • A suposição (razoável) de que se b é uma instância de B e B extends A , então b é uma instância de A

@masak Eu entendo e concordo com o princípio fechado de enums (em tempo de execução). Mas a extensão do tempo de compilação não violaria o princípio fechado em tempo de execução, pois todos seriam definidos e construídos pelo compilador.

A suposição (razoável) de que se b é uma instância de B e B estende A, então b é uma instância de A

Eu acho que esse raciocínio é meio enganoso, pois a dicotomia instância/classe não é realmente atribuível ao enum. Enums não são classes e não possuem instâncias. Eu acho, no entanto, que eles podem ser extensíveis, se feitos corretamente. Pense em enums mais como conjuntos. Neste exemplo, B é um superconjunto de A. Portanto, é razoável supor que qualquer valor em A está presente em B, mas que apenas ALGUNS valores de B estarão presentes em A.

Eu entendo de onde vem a preocupação... embora. E eu não tenho certeza do que fazer sobre isso. Um bom exemplo de um problema com a extensão enum:

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

Nesse caso, as enumerações se comportam de maneira bem diferente das classes. Se fossem classes, você esperaria ser capaz de converter B em A com bastante facilidade, mas isso claramente não funcionará aqui. Eu não acho necessariamente que isso seja RUIM, acho que deveria ser contabilizado. IE, você não pode estender um tipo de enumeração para cima em sua árvore de herança como uma classe. Isso pode ser explicado com um erro do compilador ao longo das linhas de "não é possível atribuir a enumeração do superconjunto B a A, pois nem todos os valores de B estão presentes em A".

@julian-sf

Eu acho que esse raciocínio é meio enganoso, pois a dicotomia instância/classe não é realmente atribuível ao enum. Enums não são classes e não possuem instâncias.

Você está absolutamente certo, em face disso.

  • Vista como uma construção de linguagem independente, uma enumeração tem _members_, não instâncias. O termo "membro" é usado tanto pelo Handbook quanto pela Language Specification. (C# e Python também usam o termo "membro". Java usa "constante enum", porque "membro" tem um significado sobrecarregado em Java.)
  • Visto a partir de uma perspectiva de código compilado, um enum tem _properties_, mapeando nos dois sentidos — nomes para valores e valores para nomes. Novamente, não instâncias.

Pensando nisso, percebo que estou um pouco colorido pela opinião do Java sobre enums. Em Java, os valores enum são literalmente instâncias de seu tipo enum. Em termos de implementação, um enum é uma classe que estende a classe Enum . (Você não tem permissão para fazer isso manualmente , você tem que usar a palavra-chave enum , mas isso é o que acontece nos bastidores.) O bom disso é que enums têm todas as conveniências que as classes fazem: eles pode ter campos, construtores, métodos... Nesta abordagem, membros enum _are_ instâncias. (O JLS diz isso.)

Observe que não estou propondo nenhuma alteração na semântica de enumeração do TypeScript. Em particular, não estou dizendo que o TypeScript deve mudar para usar o modelo do Java para enums. Eu _estou_ dizendo que é instrutivo/perspicaz sobrepor uma classe/instância "entendimento" em cima de enums/membros de enum. Não "um enum _é_ uma classe" ou "um membro enum _é_ uma instância"... mas há semelhanças que são transportadas.

Que semelhanças? Em primeiro lugar, digite a associação.

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

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

A última linha não verifica, porque Bar.Z não é um Foo . Novamente, isso não é _realmente_ classes e instâncias, mas pode ser _compreendido_ usando o mesmo modelo, como se Foo e Bar fossem classes e os seis membros fossem suas respectivas instâncias.

(Ignoraremos para os propósitos deste argumento o fato de que let foo: Foo = 2; também é semanticamente legal e que, em geral, os valores number são atribuíveis a variáveis ​​do tipo enum.)

Os enums têm a propriedade adicional de serem _fechados_ — desculpe, não conheço um termo melhor para isso — uma vez que você os define, não pode estendê-los. Especificamente, os membros listados dentro da declaração enum são as _only_ coisas que correspondem ao tipo enum. ("Fechado" como em "hipótese do mundo fechado".) Essa é uma ótima propriedade porque você pode verificar com total certeza que todos os casos em uma instrução switch em seu enum foram cobertos.

Com extends em enums, esta propriedade sai pela janela.

Você escreve,

Eu entendo e concordo com o princípio fechado de enums (em tempo de execução). Mas a extensão do tempo de compilação não violaria o princípio fechado em tempo de execução, pois todos seriam definidos e construídos pelo compilador.

Eu não acho que isso seja verdade, porque assume que qualquer código que estenda seu enum está em seu projeto. Mas um módulo de terceiros pode estender seu enum e, de repente, há _new_ membros do enum que também podem ser atribuídos ao seu enum, fora do controle do código que você compila. Essencialmente, enums não seriam mais fechados, nem mesmo em tempo de compilação.

Ainda me sinto um pouco desajeitado em expressar exatamente o que quero dizer, mas acredito que seja importante: extends em enum quebraria um dos recursos mais preciosos dos enums, o fato de eles serem está fechado. Por favor, conte quantos idiomas absolutamente _forbid_ estendendo/subclassificando uma enumeração, exatamente por esta razão.

Pensei um pouco mais em soluções alternativas e, trabalhando com #17592 (comentário) , você pode fazer um pouco melhor definindo também Events no espaço de valor:

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;

Dos meus testes, parece que Events.Start é interpretado corretamente como BasicEvents.Start no sistema de tipos, portanto, a verificação de exaustividade e o refinamento discriminado da união parecem funcionar bem. A principal coisa que falta é que você não pode usar Events.Pause como um tipo literal; você precisa AdvEvents.Pause . Você _pode_ usar typeof Events.Pause e resolve para AdvEvents.Pause , embora as pessoas da minha equipe tenham ficado confusas com esse tipo de padrão e eu acho que na prática eu incentivaria AdvEvents.Pause ao usar isso como um tipo.

(Isso é para o caso em que você deseja que os tipos de enumeração sejam atribuíveis entre si em vez de enums isolados. Pela minha experiência, é mais comum desejar que eles sejam atribuíveis.)

Eu acho que esta é a solução mais limpa à mão, agora.

Obrigado @alangpierce :+1:

alguma atualização disso?

@sdwvit Não sou uma das pessoas principais, mas do meu ponto de vista, a seguinte proposta de sintaxe (do OP, mas sugerida duas vezes depois) deixaria todos felizes, sem problemas conhecidos:

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

Isso deixaria _me_ feliz, porque significaria implementar esse recurso aparentemente útil "duplicar todos os membros neste outro enum" _sem_ usar extends , o que considero problemático pelos motivos que afirmei. A sintaxe ... evita esses problemas copiando, não estendendo.

A questão ainda está marcada como "Aguardando mais feedback", e eu respeito o direito dos membros principais de mantê-la nessa categoria pelo tempo que acharem necessário. Mas também, não há nada que impeça alguém de implementar o acima e enviá-lo como um PR.

@masak obrigado pela resposta. Agora tenho que passar por toda a história da discussão. Retornarei depois :)

Eu adoraria ver isso acontecer e adoraria tentar implementar isso sozinho. No entanto, ainda precisamos definir comportamentos para todas as enumerações. Tudo isso funciona bem para enumerações baseadas em strings, mas e as enumerações numéricas vanilla. Como a extensão/cópia funciona aqui?

  • Suponho que só queremos permitir estender um enum com um enum de "mesmo tipo" (numérico estende numérico, string estende string). Enums heterogêneos são tecnicamente suportados, então suponho que devemos manter esse suporte.

  • Devemos permitir a extensão de várias enumerações? Todos eles deveriam ter valores mutuamente exclusivos? Ou permitiremos valores sobrepostos? Prioridade baseada na ordem lexical?

  • As enumerações estendidas podem substituir os valores das enumerações estendidas?

  • As enumerações estendidas devem aparecer no início da lista de valores ou podem ser em qualquer ordem? Presumo que valores definidos posteriormente tenham maior prioridade?

  • Presumo que os valores numéricos implícitos continuarão 1 após o valor máximo das enumerações numéricas estendidas.

  • Considerações especiais para máscaras de bits?

etc etc.

@JeffreyMercado Essas são boas perguntas e apropriadas para quem espera tentar uma implementação. :sorrir:

Abaixo estão minhas respostas, guiadas por uma abordagem de design "conservadora" (como em "vamos tomar decisões de design que não permitem os casos sobre os quais não temos certeza, em vez de fazer escolhas agora que são difíceis de mudar mais tarde, mantendo a compatibilidade com versões anteriores").

  • Suponho que só queremos permitir estender um enum com um enum de "mesmo tipo" (numérico estende numérico, string estende string)

Eu também assumo. A enumeração resultante do tipo misto não parece super útil.

  • Devemos permitir a extensão de várias enumerações? Todos eles deveriam ter valores mutuamente exclusivos? Ou permitiremos valores sobrepostos? Prioridade baseada na ordem lexical?

Como estamos falando sobre a cópia semântica, duplicar várias enumerações parece "mais ok" do que a herança múltipla à la C++. Não vejo imediatamente um problema com isso, especialmente se continuarmos construindo a analogia do spread de objetos: let newEnum = { ...enumA, ...enumB };

Todos os membros devem ter valores mutuamente exclusivos? O conservador seria dizer "sim". Novamente, a analogia da propagação de objetos nos fornece uma semântica alternativa: o último vence.

Não consigo pensar em nenhum caso de uso em que eu gostaria de poder substituir valores de enumeração. Mas isso pode ser apenas uma falta de imaginação da minha parte. A abordagem conservadora de não permitir colisões tem as propriedades agradáveis ​​de ser fácil de explicar/internalizar e, pelo menos em teoria, pode expor erros de design reais (em código novo ou código que está sendo mantido).

  • As enumerações estendidas podem substituir os valores das enumerações estendidas?

Acho que a resposta e o raciocínio são muito parecidos neste caso como no caso anterior.

  • As enumerações estendidas devem aparecer no início da lista de valores ou podem ser em qualquer ordem? Presumo que valores definidos posteriormente tenham maior prioridade?

Eu ia dizer primeiro que isso só importa se formos com a semântica "o último vence" de substituição.

Mas pensando bem, tanto em "sem colisões" quanto em "o último vence", acho estranho até mesmo _querer_ colocar declarações de membros enum antes que enum se espalhe na lista. Tipo, qual intenção está sendo comunicada ao fazer isso? Os spreads são um pouco como "importações", e convencionalmente ficam no topo.

Eu não quero necessariamente proibir a colocação de spreads enum após declarações de membros enum (embora eu ache que ficaria bem se isso não fosse permitido na gramática). Se acabar sendo permitido, definitivamente é algo que linters e convenções da comunidade poderiam apontar como evitável. Não há apenas nenhuma razão convincente para fazê-lo.

  • Presumo que os valores numéricos implícitos continuarão 1 após o valor máximo das enumerações numéricas estendidas.

Talvez a coisa conservadora a fazer seja exigir um valor explícito para o primeiro membro após um spread.

  • Considerações especiais para máscaras de bits?

Eu acho que isso seria coberto pela regra acima.

Consegui fazer algo razoável combinando enums, interfaces e objetos imutáveis.

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 Esse é um bom uso de enums, mas... não consigo ver como isso conta como estender um enum existente. O que você está fazendo é _usando_ o enum.

(Além disso, ao contrário de enums e instruções switch, parece que no seu exemplo você não tem verificação de totalidade; alguém que adicionou um membro enum posteriormente pode facilmente esquecer de adicionar uma chave correspondente no SINGULAR e PLURAL registro em todas as instâncias de Label .)

@masak

alguém que adicionou um membro enum mais tarde pode facilmente esquecer de adicionar uma chave correspondente no registro SINGULAR e PLURAL em todas as instâncias de Label .)

Pelo menos no meu ambiente, ele gera um erro quando um membro enum está ausente de SINGULAR ou PLURAL . O tipo Record faz seu trabalho, eu acho.

Embora a documentação do TS seja boa, sinto que não há muitos exemplos de como combinar muitos recursos de maneira não trivial. enum inheritance foi a primeira coisa que procurei quando tentei resolver problemas de internacionalização, levando a este tópico. A abordagem acabou sendo errada de qualquer maneira, e é por isso que escrevi este post.

@illeatmyhat

Pelo menos no meu ambiente, ele gera um erro quando um membro enum está ausente de SINGULAR ou PLURAL . O tipo Record faz seu trabalho, eu acho.

Oh! TIL. E sim, isso o torna muito mais interessante. Entendo o que você quer dizer sobre alcançar inicialmente a herança enum e, eventualmente, aterrissar no seu padrão. Isso pode até não ser uma coisa isolada; "Problemas X/Y" são uma coisa real. Mais pessoas podem começar com o pensamento "Quero estender MyEnum ", mas acabam usando Record<MyEnum, string> como você fez.

Responda para @masak :

Com estende em enums, esta propriedade sai pela janela.

Você escreve,

@julian-sf: Eu entendo e concordo com o princípio fechado de enums (em tempo de execução). Mas a extensão do tempo de compilação não violaria o princípio fechado em tempo de execução, pois todos seriam definidos e construídos pelo compilador.

Eu não acho que isso seja verdade, porque assume que qualquer código que estenda seu enum está em seu projeto. Mas um módulo de terceiros pode estender seu enum e, de repente, há novos membros enum que também podem ser atribuídos ao seu enum, fora do controle do código que você compila. Essencialmente, enums não seriam mais fechados, nem mesmo em tempo de compilação.

Quanto mais eu penso sobre isso, você está completamente certo. Enums devem ser fechados. Gosto muito da ideia de "compor" enums, pois acho que esse é realmente o cerne da questão que queremos aqui 🥳.

Acho que essa notação resume o conceito de "juntar" duas enumerações separadas de maneira bastante elegante:

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

Então considere que minha demissão em usar o termo extends 😆


Comentários sobre as respostas de @masak às perguntas de @JeffreyMercado :

  • Suponho que só queremos permitir estender um enum com um enum de "mesmo tipo" (numérico estende numérico, string estende string). Enums heterogêneos são tecnicamente suportados, então suponho que devemos manter esse suporte.

Eu também assumo. A enumeração resultante do tipo misto não parece super útil.

Embora eu concorde que não é útil, provavelmente DEVEMOS manter o suporte heterogêneo para enums aqui. Acho que um aviso de linter seria útil aqui, mas não acho que o TS deva atrapalhar isso. Posso pensar em um caso de uso artificial que é, estou construindo uma enumeração para interações com uma API muito mal projetada que recebe sinalizadores que são uma mistura de números e strings. Inventado, eu sei, mas como é permitido em outros lugares, não acho que devamos proibir aqui.

Talvez apenas um forte incentivo para não fazer isso?

  • Devemos permitir a extensão de várias enumerações? Todos eles deveriam ter valores mutuamente exclusivos? Ou permitiremos valores sobrepostos? Prioridade baseada na ordem lexical?

Como estamos falando sobre a cópia semântica, duplicar várias enumerações parece "mais ok" do que a herança múltipla à la C++. Não vejo imediatamente um problema com isso, especialmente se continuarmos construindo a analogia da propagação de objetos: let newEnum = { ...enumA, ...enumB };

100% de acordo

  • Todos os membros devem ter valores mutuamente exclusivos?

O conservador seria dizer "sim". Novamente, a analogia da propagação de objetos nos fornece uma semântica alternativa: o último vence.

Estou rasgado aqui. Embora eu concorde que é "melhor prática" impor a exclusividade mútua de valores, está correto? É diretamente contraditório à semântica de propagação comumente conhecida. Por um lado, gosto da ideia de impor valores mutuamente exclusivos, por outro lado, quebra muitas suposições sobre como a semântica de propagação deve funcionar. Existem desvantagens em seguir as regras normais de distribuição com "o último vence"? Parece que é mais fácil na implementação (já que o objeto subjacente é apenas um mapa de qualquer maneira). Mas também parece se alinhar com as expectativas comuns. Estou inclinado a ser menos surpreendente.

Também pode haver bons exemplos para querer substituir um valor (embora eu não tenha ideia de quais seriam).

Mas pensando bem, tanto em "sem colisões" quanto em "o último vence", acho estranho até mesmo querer colocar declarações de membros enum antes que enum se espalhe na lista. Tipo, qual intenção está sendo comunicada ao fazer isso? Os spreads são um pouco como "importações", e convencionalmente ficam no topo.

Bem, isso depende, se estamos seguindo a semântica de propagação, então não importa qual seja a ordem. Honestamente, mesmo se estivermos impondo valores mutuamente exclusivos, a ordem não importaria, certo? Uma colisão seria um erro nesse ponto, independentemente da ordem.

  • Presumo que os valores numéricos implícitos continuarão 1 após o valor máximo das enumerações numéricas estendidas.

Talvez a coisa conservadora a fazer seja exigir um valor explícito para o primeiro membro após um spread.

Eu concordo. Se você espalhar uma enumeração, o TS deve apenas impor valores explícitos para membros adicionais.

@julian-sf

Então considere que minha renúncia em usar o termo se estende 😆

:+1: A Sociedade para a Preservação dos Tipos de Soma aplaude do lado de fora.

Mas pensando bem, tanto em "sem colisões" quanto em "o último vence", acho estranho até mesmo querer colocar declarações de membros enum antes que enum se espalhe na lista. Tipo, qual intenção está sendo comunicada ao fazer isso? Os spreads são um pouco como "importações", e convencionalmente ficam no topo.

Bem, isso depende, se estamos seguindo a semântica de propagação, então não importa qual seja a ordem. Honestamente, mesmo se estivermos impondo valores mutuamente exclusivos, a ordem não importaria, certo? Uma colisão seria um erro nesse ponto, independentemente da ordem.

Estou dizendo "não há uma boa razão para colocar spreads após as declarações normais dos membros"; você está dizendo "sob as restrições apropriadas, colocá-los antes ou depois não faz diferença". Ambas as coisas podem ser verdadeiras ao mesmo tempo.

A principal diferença no resultado parece cair em um espectro de permitir ou proibir spreads antes de membros normais. Pode ser sintaticamente desaprovado; poderia produzir um aviso de linter; ou poderia estar completamente bem em todos os aspectos. Se a ordem não faz diferença semântica, então tudo se resume a fazer com que o recurso enum spread siga o princípio da menor surpresa , fácil de usar e fácil de ensinar/explicar.

O uso do operador de propagação se enquadra no uso mais amplo de cópia superficial em todo JS e TypeScript. Certamente é o método mais usado e mais fácil de entender do que usar extends , o que implica uma relação direta. Criar um enum por meio da composição seria a solução mais fácil de consumir.

Algumas das sugestões de trabalho, embora válidas e utilizáveis, adicionam muito mais código clichê para alcançar o mesmo resultado desejado. Dada a natureza final e imutável de um enum, seria desejável criar enums adicionais por meio da composição, para manter as propriedades consistentes com outras linguagens.

É uma pena que 3 anos nessa conversa ainda esteja acontecendo.

@jmitchell38488 Gostaria de deixar um like no seu comentário, mas sua última frase mudou minha mente. Esta é uma discussão necessária, uma vez que a solução proposta funcionaria, mas também implica a possibilidade de estender classes e interfaces desta forma. É uma grande mudança que pode assustar alguns programadores de linguagens do tipo C++ de usar o typescript, já que você basicamente acaba com 2 maneiras de fazer a mesma coisa ( class A extends B e class A { ...(class B {}) } ). Eu acho que as duas maneiras podem ser suportadas, mas precisamos extend para enums também para consistência.

@masak wdyt? ^

@sdwvit Não estou falando sobre mudar o comportamento para criar classes e interfaces, estou falando especificamente sobre enums e criá-los por meio de composição. Eles são tipos finais imutáveis, portanto, não devemos ser capazes de estender da maneira típica de herança.

Dada a natureza do JS e o valor final transpilado, não há razão para que a composição não possa ser alcançada. Claro que tornaria o trabalho com enums mais atraente.

@masak wdyt? ^

Hum. Acho que a consistência da linguagem é um objetivo louvável e, portanto, não é _a priori_ errado pedir um recurso ... semelhante em classes e interfaces. Mas acho que o caso é mais fraco ou inexistente lá, e por duas razões: (a) classes e interfaces já têm um mecanismo de extensão, e adicionar um segundo fornece pouco valor adicional (enquanto fornecer um para enums cobriria um caso de uso que as pessoas há anos que voltam a esta questão); (b) adicionar novas sintaxes e semânticas às classes tem um nível muito mais alto de aprovação, já que as classes são, em certo sentido, da especificação EcmaScript. (O TypeScript é mais antigo que o ES6, mas tem havido um esforço ativo para que o primeiro fique próximo ao último. Isso inclui não introduzir recursos extras no topo.)

Eu acho que este tópico está aberto há muito tempo simplesmente porque representa um recurso valioso que cobriria casos de uso reais, mas ainda não foi feito um PR para ele. Fazer esse PR leva tempo e esforço, mais do que apenas dizer que você quer o recurso. :piscadela:

Alguém está trabalhando neste recurso?

Alguém está trabalhando neste recurso?

Meu palpite é que não, já que ainda nem terminamos a discussão sobre isso, haha!

Eu sinto que chegamos mais perto de um consenso sobre como isso seria. Como isso seria uma adição de linguagem, provavelmente seria necessário um "campeão" para levar essa proposta adiante. Acho que alguém da equipe TS precisaria entrar e mover o problema de "Aguardando feedback" para "Aguardando proposta" (ou algo semelhante).

Estou trabalhando em um protótipo dele. Embora eu não tenha ido muito longe por falta de tempo e desconhecimento da estrutura do código. Eu quero ver isso e se não houver outro movimento, continuarei quando puder.

Também adoraria esse recurso. 37 meses se passaram, espero que o progresso seja feito em breve

Notas da reunião recente:

  • Gostamos da sintaxe de propagação, porque extends implica um subtipo, enquanto “estender” um enum cria um supertipo.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • O pensamento é que AdvEvents.Start resolveria para a mesma identidade de tipo que BasicEvents.Start . Isso tem as implicações de que os tipos BasicEvents.Start e AdvEvents.Start seriam atribuíveis um ao outro, e o tipo BasicEvents seria atribuível a AdvEvents . Espero que isso faça sentido intuitivo, mas é importante observar que isso significa que o spread não é apenas um atalho sintático - se expandirmos o spread para o que parece significar:

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

    isso tem um comportamento diferente—aqui, BasicEvents.Start e AdvEvents.Start são _não_ atribuíveis um ao outro, devido à qualidade opaca das enumerações de string.

    Outra consequência menor desta implementação é que AdvEvents.Start provavelmente seria serializado como BasicEvents.Start em informações rápidas e emissão de declaração (pelo menos onde não há contexto sintático para vincular o membro a AdvEvents — passar o mouse sobre a expressão literal AdvEvents.Start pode resultar plausivelmente em informações rápidas que dizem AdvEvents.Start , mas pode ser mais claro exibir BasicEvents.Start mesmo assim).

  • Isso só seria permitido em enums de string.

Eu gostaria de experimentar este.

Para esclarecer: Isso não está aprovado para implementação.

Há dois comportamentos possíveis aqui, e ambos parecem ruins.

Opção 1: na verdade é açúcar

Se spread realmente significa o mesmo que copiar os membros do enum espalhado, haverá uma grande surpresa quando as pessoas tentarem usar o valor do enum estendido como se fosse o valor do enum estendido e não funcionar.

Opção 2: não é realmente açúcar

A opção preferida seria que o spread funcionasse mais como a sugestão de tipo de união de @aj-r perto do topo do encadeamento. Se esse é o comportamento que as pessoas já desejam, então as opções existentes na mesa parecem estritamente preferíveis por causa da compreensão. Caso contrário, estamos criando um novo tipo de enum de string que não se comporta como qualquer outro enum de string, o que é estranho e parece minar a "simplicidade" da sugestão aqui.

Que comportamento as pessoas querem e por quê?

Não quero a opção 1, porque leva a grandes surpresas.

Eu quero a opção 2, mas adoraria ter suporte de sintaxe suficiente para superar a desvantagem que @aj-r mencionou, para que eu pudesse escrever let e: Events = Events.Pause; do exemplo dele. A desvantagem não é terrível, mas é um lugar onde o enum estendido não pode esconder a implementação; então é meio nojento.

Eu também acho que a opção 1 é uma má ideia. Procurei referências a esse problema na minha empresa e encontrei duas revisões de código onde ele estava vinculado e, em ambos os casos (e na minha experiência pessoal), há uma necessidade clara de que os elementos do enum menor sejam atribuíveis ao tipo de enum maior . Eu me preocupo especialmente com uma pessoa introduzindo ... pensando que se comporta como a opção 2, e então a próxima pessoa fica realmente confusa (ou recorrendo a hacks como as unknown as Events.Pause ) quando casos mais complexos não funcionam.

Já existem muitas maneiras de tentar obter o comportamento da opção 2: muitos snippets neste thread, além de várias abordagens envolvendo uniões literais de string. Minha grande preocupação com a implementação da opção 1 é que ela efetivamente introduz outra maneira errada de obter a opção 2 e, portanto, leva a mais solução de problemas e frustração para as pessoas que estão aprendendo essa parte do TypeScript.

Que comportamento as pessoas querem e por quê?

Como o código fala mais alto que palavras e usando o exemplo do 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 exemplo mostra a opção 2, certo? Se sim, então eu quero desesperadamente a opção 2.

Caso contrário, estamos criando um novo tipo de enum de string que não se comporta como qualquer outro enum de string, o que é estranho e parece minar a "simplicidade" da sugestão aqui

Concordo que a opção 2 é um pouco inquietante e talvez seja melhor dar um passo atrás e pensar em alternativas. Aqui está uma exploração de como isso pode ser feito sem adicionar nenhuma nova sintaxe ou comportamento às enumerações de hoje:

Acho que minha sugestão em https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 chega mais perto nos dias de hoje e pode ser algo para trabalhar:

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

Vejo dois problemas principais com essa abordagem:

  • É realmente confuso para alguém aprendendo TypeScript; se você não tiver uma compreensão sólida do espaço de tipo versus o espaço de valor, parece que está substituindo um tipo por uma constante.
  • Está incompleto, pois não permite que você use Events.Pause como um tipo.

Anteriormente, sugeri https://github.com/microsoft/TypeScript/issues/29130 que (entre outras coisas) abordaria o segundo marcador e acho que ainda pode ser valioso, embora certamente adicione muita sutileza a como os nomes funcionam.

Uma ideia de sintaxe que acho que abordaria ambos os pontos é uma sintaxe alternativa const que também declara um 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;

Isso se aproximaria de um mundo onde enums não são necessários. (Para mim, enums sempre pareceram ir contra os objetivos de design do TS, pois são uma sintaxe de nível de expressão com comportamento de emissão não trivial e nominal por padrão.) A sintaxe de declaração const type também permite crie um enum de string estruturalmente tipado como este:

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

Reconhecidamente, a maneira como isso precisaria funcionar é que o tipo BasicEvents é uma abreviação de typeof BasicEvents[keyof typeof BasicEvents] , que pode ser muito focado para uma sintaxe de nome genérico como const type . Talvez uma palavra-chave diferente seja melhor. Pena que const enum já foi levado 😛.

A partir daí, acho que a única lacuna entre enums (string) e literais de objeto seria a tipagem nominal. Isso possivelmente poderia ser resolvido usando uma sintaxe como as unique ou const unique type que basicamente opta pelo comportamento de digitação nominal do tipo enum para esses tipos de objeto e, idealmente, também para constantes de string regulares. Isso também daria uma escolha clara entre a opção 1 e a opção 2 ao definir Events : você usa unique quando pretende que Events seja um tipo completamente distinto (opção 1), e você omite unique quando deseja a atribuição entre Events e BasicEvents (opção 2).

Com const type e const unique type , haveria uma maneira de unir enums existentes de forma limpa e também uma maneira de expressar enums como uma combinação natural de recursos do TS em vez de uma única.

O que está acontecendo aqui? 😅

uau de 2017 🤪, que mais feedback você precisa?

que mais feedback você precisa?

Bem aqui??? Não implementamos sugestões apenas porque são antigas!

que mais feedback você precisa?

Bem aqui??? Não implementamos sugestões apenas porque são antigas!

sim. Eu não tinha certeza de que caminho era.

Lendo novamente e vendo também #40998 acho que é a melhor maneira... os emuns são objetos e acho que o spread é mais fácil de mesclar/estender enums.

Eu não acho que estou qualificado o suficiente para oferecer minha opinião sobre design de linguagem, mas acho que posso dar feedback como desenvolvedor regular.

Eu me deparei com esse problema algumas semanas antes em um projeto real onde eu queria usar esse recurso. Acabei usando a abordagem do @alangpierce . Infelizmente, devido às minhas responsabilidades com meu empregador, não posso compartilhar o código aqui, mas aqui estão alguns pontos:

  1. Repetir declarações (tipo e const para um novo enum) não era um grande problema e não danificava muito a legibilidade.
  2. No meu caso, enum representava ações diferentes para um determinado algoritmo e havia diferentes conjuntos de ações para diferentes situações nesse algoritmo. Usar a hierarquia de tipos me permitiu verificar que certas ações não poderiam acontecer em tempo de compilação: esse era o objetivo da coisa toda e acabou sendo bastante útil.
  3. O código que escrevi era uma biblioteca interna e, embora a distinção entre os diferentes tipos de enumeração fosse importante dentro da biblioteca, isso não importava para os usuários externos dessa biblioteca. Usando essa abordagem, consegui exportar apenas um tipo que era o tipo de soma de todos os diferentes enums internos e ocultar os detalhes da implementação.
  4. Infelizmente, não consegui descobrir uma maneira idiomática e fácil de ler para analisar valores do tipo soma automaticamente a partir das declarações de tipo. (No meu caso, diferentes etapas do algoritmo que mencionei foram carregadas do banco de dados SQL em tempo de execução). Não era um grande problema (escrever o código de análise manualmente era bastante simples), mas seria bom se a implementação da herança enum prestasse alguma atenção a esse problema.

No geral, acho que muitos projetos reais com lógica de negócios chata se beneficiariam muito com esse recurso. Dividir enums em diferentes subtipos permitiria que o sistema de tipos verificasse muitas invariantes que agora são verificadas por testes de unidade, e tornar os valores incorretos irrepresentáveis ​​por um sistema de tipos é sempre uma coisa boa.

Oi,

Deixe-me adicionar meus dois centavos aqui 🙂

Meu contexto

Tenho uma API, com a documentação do OpenApi gerada com tsoa .

Um dos meus modelos tem um status definido assim:

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

Eu tenho um método setStatus que recebe um subconjunto desses status. Como o recurso não está disponível, considerei duplicar o enum dessa maneira:

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

Então meu método é descrito desta forma:

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

com esse código, recebo este erro:

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.

que vou fazer por enquanto, mas fiquei curioso e comecei a pesquisar neste repositório, quando encontrei isso.
Depois de percorrer todo o caminho, não encontrei ninguém (ou talvez tenha perdido alguma coisa) que sugira esse caso de uso.

No meu caso de uso, não quero "estender" de um enum porque não há razão para que EntityStatus seja uma extensão de RequestedEntityStatus. Eu preferiria poder "Escolha" do enum mais genérico.

Minha proposta

Achei o operador spread melhor do que a proposta extends, mas gostaria de ir mais longe e sugerir estes:

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 forma mais geral, isso deve 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

Em outras palavras

Eu gostaria de relaxar as restrições nos tipos de enumeração para se comportarem mais como uniões ( 'a' | 'b' ) do que estruturas opacas.

Ao adicionar essas habilidades ao compilador, duas enumerações independentes com os mesmos valores podem ser atribuídas uma à outra com as mesmas regras das uniões:
Dadas as seguintes enumerações:

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

E três variáveis a:A , b:B e c:C

  • c = a deve funcionar, porque A é apenas um subconjunto de C, então todo valor de A é um valor válido de C
  • c = b deve funcionar, já que B é apenas 'a' | 'c' que são ambos valores válidos de C
  • b = a pode funcionar se a for diferente de 'b' (que seria igual ao tipo 'a' apenas)
  • a = b , da mesma forma, pode funcionar se b for diferente de 'c'
  • b = c pode funcionar se c for diferente de 'b' (que seria igual a 'a'|'c' , que é exatamente o que B é)

ou talvez devêssemos precisar de um elenco explícito no lado direito, como para a comparação de igualdade?

Sobre conflitos de membros enum

Não sou fã da regra das "últimas vitórias", mesmo que pareça natural com o operador de spread.
Eu diria que o compilador deve retornar um erro se uma "chave" ou um "valor" do enum estiver duplicado, a menos que a chave e o valor sejam 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'

Fechamento

Acho essa proposta bastante flexível e natural para os desenvolvedores de TS trabalharem, enquanto permite mais segurança de tipo (em comparação com as unknown as T ) sem realmente alterar o que é um enum. Ele apenas apresenta uma nova maneira de adicionar membros ao enum e uma nova maneira de comparar enums entre si.
O que você acha? Eu perdi algum problema óbvio de arquitetura de linguagem que torna esse recurso inatingível?

Esta página foi útil?
0 / 5 - 0 avaliações