Typescript: Memperluas enum berbasis string

Dibuat pada 3 Agu 2017  ·  68Komentar  ·  Sumber: microsoft/TypeScript

Sebelum enum berbasis string, banyak yang akan kembali ke objek. Menggunakan objek juga memungkinkan perluasan tipe. Sebagai contoh:

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

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

Saat beralih ke string enum, tidak mungkin mencapai ini tanpa mendefinisikan ulang enum.

Saya akan sangat berguna untuk dapat melakukan sesuatu seperti ini:

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

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

Mempertimbangkan bahwa enum yang dihasilkan adalah objek, ini juga tidak akan terlalu mengerikan:

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

Komentar yang paling membantu

Semua solusi bagus tetapi saya ingin melihat dukungan warisan enum dari TypeScript itu sendiri sehingga saya dapat menggunakan pemeriksaan lengkap sesederhana mungkin.

Semua 68 komentar

Hanya bermain dengannya sedikit dan saat ini dimungkinkan untuk melakukan ekstensi ini menggunakan objek untuk tipe yang diperluas, jadi ini akan berfungsi dengan baik:

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

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

Catatan, Anda bisa dekat dengan

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

Pilihan lain, tergantung pada kebutuhan Anda, adalah menggunakan tipe serikat pekerja:

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

let e: Events = AdvEvents.Pause;

Kelemahannya adalah Anda tidak dapat menggunakan Events.Pause ; anda harus menggunakan AdvEvents.Pause . Jika Anda menggunakan const enums, ini mungkin baik-baik saja. Jika tidak, itu mungkin tidak cukup untuk kasus penggunaan Anda.

Kami membutuhkan fitur ini untuk reduksi Redux yang diketik dengan kuat. Silakan tambahkan di TypeScript.

Solusi lain adalah tidak menggunakan enum, tetapi menggunakan sesuatu yang terlihat seperti 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];

Semua solusi bagus tetapi saya ingin melihat dukungan warisan enum dari TypeScript itu sendiri sehingga saya dapat menggunakan pemeriksaan lengkap sesederhana mungkin.

Cukup gunakan kelas alih-alih enum.

Saya hanya mencoba ini.

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

Harus ada cara yang lebih baik untuk melakukan ini.

Mengapa ini belum menjadi fitur? Tidak ada perubahan yang mengganggu, perilaku intuitif, lebih dari 80 orang yang secara aktif mencari dan menuntut fitur ini – sepertinya tidak perlu dipikirkan lagi.

Bahkan mengekspor ulang enum dari file yang berbeda di namespace benar-benar aneh tanpa memperluas enum (dan tidak mungkin mengekspor kembali enum dengan cara masih enum dan bukan objek dan jenis):

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
Saat ini menggunakan solusi, tetapi ini harus menjadi fitur enum asli.

Saya membaca sekilas masalah ini untuk melihat apakah ada yang mengajukan pertanyaan berikut. (Kelihatannya tidak.)

Dari OP:

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

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

Apakah orang akan mengharapkan AdvEvents dapat ditetapkan ke BasicEvents ? (Seperti, misalnya, kasus dengan extends untuk kelas.)

Jika ya, lalu seberapa baik itu cocok dengan fakta bahwa tipe enum dimaksudkan untuk menjadi final dan tidak mungkin untuk diperpanjang?

@masak poin bagus. Fitur yang diinginkan orang di sini jelas tidak seperti biasanya extends . BasicEvents harus dapat ditetapkan ke AdvEvents , bukan sebaliknya. Normal extends menyaring tipe lain menjadi lebih spesifik, dan dalam hal ini kami ingin memperluas tipe lain untuk menambahkan lebih banyak nilai, jadi sintaks khusus apa pun untuk ini mungkin tidak boleh menggunakan kata kunci extends , atau setidaknya tidak menggunakan sintaks enum A extends B { .

Pada catatan itu, saya menyukai saran spread untuk ini dari OP.

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

Karena penyebaran sudah membawa harapan bahwa yang asli dikloning secara dangkal menjadi salinan yang tidak berhubungan.

BasicEvents harus dapat ditetapkan ke AdvEvents , bukan sebaliknya.

Saya dapat melihat bagaimana itu bisa benar dalam semua kasus, tetapi saya tidak yakin itu harus benar dalam semua kasus, jika Anda mengerti maksud saya. Terasa seperti itu akan bergantung pada domain dan bergantung pada alasan nilai enum tersebut disalin.

Saya memikirkan solusi sedikit lagi, dan bekerja dari https://github.com/Microsoft/TypeScript/issues/17592#issuecomment -331491147 , Anda dapat melakukan sedikit lebih baik dengan juga mendefinisikan Events dalam nilai ruang angkasa:

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;

Dari pengujian saya, sepertinya Events.Start ditafsirkan dengan benar sebagai BasicEvents.Start dalam sistem tipe, jadi pemeriksaan kelengkapan dan penyempurnaan serikat yang didiskriminasi tampaknya berfungsi dengan baik. Hal utama yang hilang adalah Anda tidak dapat menggunakan Events.Pause sebagai tipe literal; Anda membutuhkan AdvEvents.Pause . Anda dapat menggunakan typeof Events.Pause dan diselesaikan menjadi AdvEvents.Pause , meskipun orang-orang di tim saya bingung dengan pola semacam itu dan saya pikir dalam praktiknya saya akan mendorong AdvEvents.Pause saat menggunakan itu sebagai tipe.

(Ini untuk kasus ketika Anda ingin jenis enum dapat ditetapkan antara satu sama lain daripada enum yang terisolasi. Dari pengalaman saya, paling umum ingin mereka dapat dialihkan.)

Saran lain (walaupun tidak menyelesaikan masalah aslinya), bagaimana dengan menggunakan string literal untuk membuat tipe union?

type BEs = "Start" | "Finish";

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

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

Jadi, solusi untuk masalah kita bisa jadi ini?

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

Memperluas enum harus menjadi fitur inti dari TypeScript. Katakan saja

@wottpal Mengulangi pertanyaan saya dari sebelumnya:

Jika [enum dapat diperpanjang], lalu seberapa baik itu cocok dengan fakta bahwa tipe enum dimaksudkan untuk menjadi final dan tidak mungkin untuk diperpanjang?

Secara khusus, bagi saya tampaknya pemeriksaan totalitas pernyataan sakelar atas nilai enum tergantung pada non-perpanjangan enum.

@masak Apa? Tidak! Karena enum yang diperluas adalah tipe yang lebih luas dan tidak dapat ditetapkan ke enum asli, Anda selalu tahu semua nilai dari setiap enum yang Anda gunakan. Memperluas dalam konteks ini berarti membuat enum baru, bukan mengubah yang lama.

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, jadi maksud Anda extends di sini sebenarnya memiliki lebih banyak _copying_ semantik (dari nilai enum dari A menjadi B )? Kemudian, ya, sakelarnya keluar dengan baik.

Namun, ada _beberapa_ harapan di sana yang tampaknya masih hancur bagi saya. Sebagai cara untuk mencoba dan menyelesaikannya: dengan kelas, extends tidak _not_ menyampaikan penyalinan semantik — bidang dan metode tidak disalin ke subkelas yang diperluas; sebaliknya, mereka hanya tersedia melalui rantai prototipe. Hanya ada satu bidang atau metode, di superclass.

Karena itu, jika class B extends A , kami dijamin bahwa B dapat dialihkan ke A , dan misalnya let a: A = new B(); akan baik-baik saja.

Tetapi dengan enums dan extends , kami tidak akan dapat melakukan let a: A = B.b; , karena tidak ada jaminan yang sesuai. Itulah yang terasa aneh bagi saya; extends di sini menyampaikan serangkaian asumsi tertentu tentang apa yang dapat dilakukan, dan asumsi tersebut tidak dipenuhi dengan enum.

Lalu sebut saja expands atau clones ? ️
Dari sudut pandang pengguna, rasanya aneh bahwa sesuatu yang mendasar tidak mudah dicapai.

Jika semantik yang masuk akal membutuhkan kata kunci yang sama sekali baru (tanpa banyak seni sebelumnya dalam bahasa lain), mengapa tidak menggunakan kembali sintaks spread ( ... ) seperti yang disarankan dalam OP dan komentar ini ?

+1 Saya harap ini akan ditambahkan ke fitur enumerasi default. :)

Adakah yang tahu solusi elegan??? 🧐.

Jika semantik yang masuk akal membutuhkan kata kunci yang sama sekali baru (tanpa banyak seni sebelumnya dalam bahasa lain), mengapa tidak menggunakan kembali sintaks spread (...) seperti yang disarankan dalam OP dan komentar ini?

Ya, setelah memikirkannya sedikit lebih lama, saya pikir solusi ini akan bagus.

Setelah membaca seluruh utas masalah ini, tampaknya ada kesepakatan luas bahwa menggunakan kembali operator spread memecahkan masalah, dan mengatasi semua kekhawatiran yang diajukan orang tentang membuat sintaks membingungkan/tidak intuitif.

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

Apakah masalah ini benar-benar masih membutuhkan label "Menunggu Lebih Banyak Umpan Balik" pada saat ini, @RyanCavanaugh ?

Fitur ingin +1

Apakah kita punya berita tentang masalah ini? Rasanya sangat berguna untuk menerapkan operator spread untuk enum.

Khusus untuk kasus penggunaan yang melibatkan metaprogramming, kemampuan untuk alias dan memperluas enum berada di antara must-have dan nice-to-have. Saat ini tidak ada cara untuk mengambil satu enum dan export dengan nama lain - kecuali jika Anda menggunakan salah satu solusi yang disebutkan di atas.

@m93a Ah, jadi maksud Anda extends di sini sebenarnya memiliki lebih banyak _copying_ semantik (dari nilai enum dari A menjadi B )? Kemudian, ya, sakelarnya keluar dengan baik.

Namun, ada _beberapa_ harapan di sana yang tampaknya masih hancur bagi saya. Sebagai cara untuk mencoba dan menyelesaikannya: dengan kelas, extends tidak _not_ menyampaikan penyalinan semantik — bidang dan metode tidak disalin ke subkelas yang diperluas; sebaliknya, mereka hanya tersedia melalui rantai prototipe. Hanya ada satu bidang atau metode, di superclass.

Karena itu, jika class B extends A , kami dijamin bahwa B dapat dialihkan ke A , dan misalnya let a: A = new B(); akan baik-baik saja.

Tetapi dengan enums dan extends , kami tidak akan dapat melakukan let a: A = B.b; , karena tidak ada jaminan yang sesuai. Itulah yang terasa aneh bagi saya; extends di sini menyampaikan serangkaian asumsi tertentu tentang apa yang dapat dilakukan, dan asumsi tersebut tidak dipenuhi dengan enum.

@masak Saya pikir Anda hampir benar tetapi Anda membuat satu asumsi kecil yang salah. B dapat ditetapkan ke A jika enum B extends A sebagai "dapat dialihkan" berarti bahwa semua nilai yang diberikan oleh A tersedia di B. Ketika Anda mengatakan bahwa let a: A = B.b Anda mengasumsikan bahwa nilai dalam B harus tersedia di A, yang tidak sama dengan nilai yang dapat ditetapkan ke A. let a: A = B.a ADALAH benar karena B dapat ditetapkan ke A.

Ini terbukti dengan menggunakan kelas seperti pada contoh berikut:

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`

Tautan Taman Bermain TypeScript

invalid access

Singkat cerita, saya percaya memperluas IS terminologi yang benar karena itulah yang sedang dilakukan. Dalam contoh enum B extends A Anda SELALU dapat mengharapkan B enum berisi semua nilai yang mungkin dari A enum, karena B adalah "subkelas" (subenum? mungkin ada kata yang lebih baik untuk ini) dari A dan dengan demikian ditugaskan ke A

Jadi saya rasa kita tidak memerlukan kata kunci baru, saya pikir kita harus menggunakan ekstensi DAN saya pikir ini seharusnya menjadi bagian dari TypeScript :D

@julian-sf Saya pikir saya setuju dengan setiap hal yang Anda tulis...

...tapi... :slightly_smiling_face:

seperti yang saya permasalahkan di sini , bagaimana dengan situasi ini?

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

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

Mengingat bahwa Pause adalah _instance_ dari AdvEvents dan AdvEvents _extends_ BasicEvents , apakah Anda juga mengharapkan Pause menjadi _instance_ dari BasicEvents ? (Karena itu tampaknya mengikuti dari bagaimana hubungan instance/warisan biasanya berinteraksi.)

Di sisi lain, proposisi nilai inti enum (IMHO) adalah bahwa mereka _closed_/"final" (seperti dalam, tidak dapat diperluas) sehingga sesuatu seperti pernyataan switch dapat mengasumsikan totalitas. (Jadi AdvEvents dapat _memperluas_ apa artinya menjadi BasicEvent melanggar semacam Kejutan Terkecil untuk enum.)

Saya tidak berpikir bahwa Anda dapat lebih dari dua dari tiga properti berikut:

  • Enum sedang ditutup/final/diprediksi total
  • Relasi extends antara dua deklarasi enum
  • Asumsi (masuk akal) bahwa jika b adalah turunan dari B dan B extends A , maka b adalah turunan dari A

@masak Saya mengerti dan setuju dengan prinsip tertutup enum (saat runtime). Tetapi perpanjangan waktu kompilasi tidak akan melanggar prinsip tertutup saat runtime, karena semuanya akan ditentukan dan dibangun oleh kompiler.

Asumsi (masuk akal) bahwa jika b adalah turunan dari B dan B memperluas A, maka b adalah turunan dari A

Saya pikir alasan ini agak menyesatkan karena dikotomi instance/kelas tidak benar-benar dapat ditugaskan ke enum. Enum bukan kelas dan mereka tidak memiliki instance. Saya pikir bagaimanapun, bahwa mereka dapat diperpanjang, jika dilakukan dengan benar. Pikirkan enum lebih seperti set. Dalam contoh ini, B adalah superset dari A. Oleh karena itu, masuk akal untuk mengasumsikan bahwa nilai apa pun di A ada di B, tetapi hanya BEBERAPA nilai B yang akan ada di A.

Saya mengerti dari mana kekhawatiran itu berasal ... meskipun. Dan saya tidak yakin apa yang harus dilakukan tentang itu. Contoh bagus dari masalah dengan ekstensi 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...

Dalam hal ini, enum berperilaku sangat berbeda dari kelas. Jika ini adalah kelas, Anda akan berharap dapat melemparkan B ke A dengan cukup mudah, tetapi itu jelas tidak akan berhasil di sini. Saya tidak selalu berpikir ini BURUK, saya pikir itu harus dipertanggungjawabkan. IE, Anda tidak dapat memasukkan tipe enum ke atas di pohon warisannya seperti kelas. Ini dapat dijelaskan dengan kesalahan kompiler di sepanjang baris "tidak dapat menetapkan superset enum B ke A, karena tidak semua nilai B ada di A".

@julian-sf

Saya pikir alasan ini agak menyesatkan karena dikotomi instance/kelas tidak benar-benar dapat ditugaskan ke enum. Enum bukan kelas dan mereka tidak memiliki instance.

Anda benar sekali, di muka itu.

  • Dilihat sebagai konstruksi bahasa independen, enumerasi memiliki _members_, bukan instance. Istilah "anggota" digunakan baik oleh Buku Pegangan maupun Spesifikasi Bahasa. (C# dan Python juga menggunakan istilah "anggota". Java menggunakan "konstanta enum", karena "anggota" memiliki arti yang berlebihan di Jawa.)
  • Dilihat dari perspektif kode yang dikompilasi, enum memiliki _properties_, memetakan dua arah — nama-ke-nilai dan nilai-ke-nama. Sekali lagi, bukan contoh.

Memikirkan hal ini, saya menyadari bahwa saya sedikit diwarnai oleh pandangan Java tentang enum. Di Jawa, nilai enum secara harfiah adalah contoh dari tipe enumnya. Dari segi implementasi, enum adalah kelas yang memperluas kelas Enum . (Anda tidak diizinkan untuk melakukannya secara manual , Anda harus melalui kata kunci enum , tapi itulah yang terjadi di bawah tenda.) Hal yang menyenangkan tentang ini adalah enum mendapatkan semua kemudahan yang dilakukan kelas: mereka dapat memiliki bidang, konstruktor, metode... Dalam pendekatan ini, anggota enum _are_ instance. (JLS mengatakan sebanyak itu.)

Perhatikan bahwa saya tidak mengusulkan perubahan apa pun pada semantik enum TypeScript. Secara khusus, saya tidak mengatakan TypeScript harus berubah menggunakan model Java untuk enum. Saya _am_ mengatakan bahwa itu instruktif/wawasan untuk melapisi "pemahaman" kelas/contoh di atas anggota enum/enum. Bukan "enum _is_ kelas" atau "anggota enum _is_ sebuah instance"... tetapi ada kesamaan yang terbawa.

Kesamaan apa? Pertama dan terpenting, ketik keanggotaan.

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

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

Baris terakhir tidak mengetik, karena Bar.Z bukan Foo . Sekali lagi, ini bukan _sebenarnya_ kelas dan instance, tetapi dapat _dipahami_ menggunakan model yang sama, seolah-olah Foo dan Bar adalah kelas dan enam anggota adalah instance masing-masing.

(Kami akan mengabaikan untuk tujuan argumen ini fakta bahwa let foo: Foo = 2; juga legal secara semantik, dan bahwa secara umum, nilai number dapat ditetapkan ke variabel tipe enum.)

Enum memiliki properti tambahan yang mereka _closed_ — maaf, saya tidak tahu istilah yang lebih baik untuk ini — setelah Anda mendefinisikannya, Anda tidak dapat memperluasnya. Secara khusus, anggota yang terdaftar di dalam deklarasi enum adalah _hanya_ hal yang typematch dengan jenis enum. ("Tertutup" seperti dalam "hipotesis dunia tertutup".) Ini adalah properti yang bagus karena Anda dapat memverifikasi dengan kepastian total bahwa semua kasus dalam pernyataan switch pada enum Anda telah tercakup.

Dengan extends pada enum, properti ini keluar dari jendela.

Anda menulis,

Saya mengerti dan setuju dengan prinsip tertutup enum (saat runtime). Tetapi perpanjangan waktu kompilasi tidak akan melanggar prinsip tertutup saat runtime, karena semuanya akan ditentukan dan dibangun oleh kompiler.

Saya tidak berpikir itu benar, karena mengasumsikan bahwa kode apa pun yang memperluas enum Anda ada di proyek Anda. Tetapi modul pihak ketiga dapat memperluas enum Anda, dan tiba-tiba ada _new_ anggota enum yang juga dapat ditugaskan ke enum Anda, di luar kendali kode yang Anda kompilasi. Pada dasarnya, enum tidak akan lagi ditutup, bahkan pada waktu kompilasi.

Saya masih merasa agak canggung dalam mengungkapkan apa yang saya maksud, tetapi saya percaya ini penting: extends pada enum akan merusak salah satu fitur paling berharga dari enum, fakta bahwa mereka' kembali ditutup. Harap hitung berapa banyak bahasa yang benar-benar _melarang_ memperluas/mensubklasifikasikan enum, karena alasan ini.

Saya memikirkan solusi sedikit lagi, dan bekerja dari #17592 (komentar) , Anda dapat melakukan sedikit lebih baik dengan juga mendefinisikan Events di ruang nilai:

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;

Dari pengujian saya, sepertinya Events.Start ditafsirkan dengan benar sebagai BasicEvents.Start dalam sistem tipe, jadi pemeriksaan kelengkapan dan penyempurnaan serikat yang didiskriminasi tampaknya berfungsi dengan baik. Hal utama yang hilang adalah Anda tidak dapat menggunakan Events.Pause sebagai tipe literal; Anda membutuhkan AdvEvents.Pause . Anda _can_ menggunakan typeof Events.Pause dan diselesaikan menjadi AdvEvents.Pause , meskipun orang-orang di tim saya bingung dengan pola semacam itu dan saya pikir dalam praktiknya saya akan mendorong AdvEvents.Pause saat menggunakan itu sebagai tipe.

(Ini untuk kasus ketika Anda ingin jenis enum dapat ditetapkan antara satu sama lain daripada enum yang terisolasi. Dari pengalaman saya, paling umum ingin mereka dapat dialihkan.)

Saya pikir ini adalah solusi paling rapi yang ada, saat ini.

Terima kasih @alangpierce :+1:

ada update tentang ini?

@sdwvit Saya bukan salah satu dari orang-orang inti, tetapi dari sudut pandang saya, proposal sintaks berikut (dari OP, tetapi disarankan kembali dua kali setelah itu) akan membuat semua orang senang, tanpa masalah yang diketahui:

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

Itu akan membuat _me_ senang, karena itu berarti menerapkan fitur "duplikat semua anggota di enum lain" yang tampaknya berguna ini _tanpa_ menggunakan extends , yang saya anggap bermasalah karena alasan yang telah saya nyatakan. Sintaks ... menghindari masalah ini dengan menyalin, bukan memperluas.

Masalah ini masih ditandai sebagai "Menunggu Lebih Banyak Umpan Balik", dan saya menghormati hak anggota inti untuk menyimpannya dalam kategori itu selama mereka merasa perlu. Tetapi juga, tidak ada yang menghentikan siapa pun untuk menerapkan hal di atas dan mengirimkannya sebagai PR.

@masak terima kasih atas tanggapannya. Saya sekarang harus melalui semua sejarah diskusi. Akan kembali kepada Anda setelah :)

Saya benar-benar akan senang melihat ini terjadi dan akan sangat senang untuk mencoba menerapkannya sendiri. Namun kita masih perlu mendefinisikan perilaku untuk semua enum. Ini semua berfungsi dengan baik untuk enum berbasis string, tetapi bagaimana dengan enum numerik vanilla. Bagaimana cara memperluas/menyalin bekerja di sini?

  • Saya berasumsi kita hanya ingin mengizinkan perluasan enum dengan enum "tipe yang sama" (numeric extends numeric, string extends string). Enum heterogen secara teknis didukung jadi saya kira kita harus mempertahankan dukungan itu.

  • Haruskah kita mengizinkan perluasan dari beberapa enum? Haruskah mereka semua memiliki nilai yang saling eksklusif? Atau akankah kita mengizinkan nilai yang tumpang tindih? Prioritas berdasarkan urutan leksikal?

  • Bisakah enum yang diperluas mengesampingkan nilai dari enum yang diperluas?

  • Haruskah enum yang diperluas muncul sebagai awal dari daftar nilai atau dapatkah dalam urutan apa pun? Saya berasumsi nilai yang ditentukan kemudian memiliki prioritas lebih tinggi?

  • Saya berasumsi nilai numerik implisit akan berlanjut 1 setelah nilai maksimal enum numerik yang diperluas.

  • Pertimbangan khusus untuk bit-masker?

dll. dll.

@JeffreyMercado Ini adalah pertanyaan yang bagus, dan cocok untuk orang yang berharap untuk mencoba implementasi. :senyum:

Di bawah ini adalah jawaban saya, dipandu oleh pendekatan desain "konservatif" (seperti dalam "mari membuat keputusan desain yang melarang kasus yang tidak kami yakini, daripada membuat pilihan sekarang yang sulit diubah nanti sambil tetap kompatibel").

  • Saya berasumsi kita hanya akan mengizinkan perpanjangan enum dengan enum "tipe yang sama" (numerik meluas numerik, string meluas string)

Saya berasumsi begitu juga. Enum tipe campuran yang dihasilkan tampaknya tidak terlalu berguna.

  • Haruskah kita mengizinkan perluasan dari beberapa enum? Haruskah mereka semua memiliki nilai yang saling eksklusif? Atau akankah kita mengizinkan nilai yang tumpang tindih? Prioritas berdasarkan urutan leksikal?

Karena menyalin semantik yang sedang kita bicarakan, menduplikasi banyak enum tampaknya "lebih baik" daripada Multiple Inheritance la C++. Saya tidak langsung melihat masalah dengan itu, terutama jika kita terus membangun analogi penyebaran objek: let newEnum = { ...enumA, ...enumB };

Haruskah semua anggota memiliki nilai yang saling eksklusif? Hal yang konservatif adalah mengatakan "ya". Sekali lagi, analogi penyebaran objek memberi kita semantik alternatif: yang terakhir menang.

Saya tidak dapat memikirkan kasus penggunaan apa pun di mana saya akan menghargai kemampuan untuk mengganti nilai enum. Tapi itu mungkin hanya kurangnya imajinasi di pihak saya. Pendekatan konservatif untuk melarang tabrakan memiliki properti menyenangkan yang mudah dijelaskan/diinternalisasi, dan setidaknya secara teori itu mungkin mengekspos kesalahan desain nyata (dalam kode baru, atau kode yang sedang dipertahankan).

  • Bisakah enum yang diperluas mengesampingkan nilai dari enum yang diperluas?

Saya pikir jawabannya, dan alasannya, hampir sama dalam kasus ini seperti dalam kasus sebelumnya.

  • Haruskah enum yang diperluas muncul sebagai awal dari daftar nilai atau dapatkah dalam urutan apa pun? Saya berasumsi nilai yang ditentukan kemudian memiliki prioritas lebih tinggi?

Saya akan mengatakan pertama bahwa ini hanya penting jika kita menggunakan semantik "yang terakhir menang" untuk mengesampingkan.

Tetapi setelah dipikir-pikir, baik di bawah "tidak ada tabrakan" dan "yang terakhir menang", saya merasa aneh bahkan _ingin_ menempatkan deklarasi anggota enum sebelum enum menyebar dalam daftar. Seperti, maksud apa yang dikomunikasikan dengan melakukan itu? Spreadnya sedikit seperti "impor", dan ini secara konvensional berada di atas.

Saya tidak selalu ingin melarang penyebaran enum setelah deklarasi anggota enum (walaupun saya pikir saya akan baik-baik saja dengan itu dilarang dalam tata bahasa). Jika akhirnya diizinkan, itu pasti sesuatu yang dapat dihindari oleh linter dan konvensi komunitas. Tidak ada alasan kuat untuk melakukannya.

  • Saya berasumsi nilai numerik implisit akan berlanjut 1 setelah nilai maksimal enum numerik yang diperluas.

Mungkin hal konservatif yang harus dilakukan adalah meminta nilai eksplisit untuk anggota pertama setelah spread.

  • Pertimbangan khusus untuk bit-masker?

Saya pikir itu akan tercakup oleh aturan di atas.

Saya dapat melakukan sesuatu yang masuk akal dengan menggabungkan enum, antarmuka, dan objek yang tidak dapat diubah.

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 Itu penggunaan enum yang bagus, tapi... Saya gagal melihat bagaimana hal itu dianggap sebagai perpanjangan enum yang ada. Apa yang Anda lakukan adalah _using_ enum.

(Juga, tidak seperti enum dan pernyataan switch, tampaknya dalam contoh Anda, Anda tidak memiliki pemeriksaan totalitas; seseorang yang menambahkan anggota enum nanti mungkin dengan mudah lupa menambahkan kunci yang sesuai di SINGULAR dan PLURAL rekam di semua instance Label .)

@masak

seseorang yang menambahkan anggota enum nanti mungkin dengan mudah lupa untuk menambahkan kunci yang sesuai dalam catatan SINGULAR dan PLURAL dalam semua contoh Label .)

Setidaknya di lingkungan saya, itu menimbulkan kesalahan ketika anggota enum hilang dari SINGULAR atau PLURAL . Jenis Record melakukan tugasnya, saya kira.

Walaupun dokumentasi untuk TS bagus, saya merasa tidak banyak contoh bagaimana menggabungkan banyak fitur bersama dengan cara yang tidak sepele. enum inheritance adalah hal pertama yang saya cari ketika saya mencoba memecahkan masalah internasionalisasi, yang mengarah ke utas ini. Pendekatannya ternyata salah, itulah sebabnya saya menulis posting ini.

@illeatmyhat

Setidaknya di lingkungan saya, itu menimbulkan kesalahan ketika anggota enum hilang dari SINGULAR atau PLURAL . Jenis Record melakukan tugasnya, saya kira.

Oh! TIL. Dan ya, itu membuatnya jauh lebih menarik. Saya mengerti apa yang Anda maksud tentang awalnya meraih warisan enum dan akhirnya mendarat di pola Anda. Itu bahkan mungkin bukan hal yang terisolasi; "Masalah X/Y" adalah hal yang nyata. Lebih banyak orang mungkin mulai dengan pemikiran "Saya ingin memperpanjang MyEnum ", tetapi akhirnya menggunakan Record<MyEnum, string> seperti yang Anda lakukan.

Balas ke @masak :

Dengan ekstensi pada enum, properti ini keluar dari jendela.

Anda menulis,

@julian-sf: Saya mengerti dan setuju dengan prinsip tertutup enum (saat runtime). Tetapi perpanjangan waktu kompilasi tidak akan melanggar prinsip tertutup saat runtime, karena semuanya akan ditentukan dan dibangun oleh kompiler.

Saya tidak berpikir itu benar, karena mengasumsikan bahwa kode apa pun yang memperluas enum Anda ada di proyek Anda. Tetapi modul pihak ketiga dapat memperluas enum Anda, dan tiba-tiba ada anggota enum baru yang juga dapat ditugaskan ke enum Anda, di luar kendali kode yang Anda kompilasi. Pada dasarnya, enum tidak akan lagi ditutup, bahkan pada waktu kompilasi.

Semakin saya memikirkan hal ini, Anda sepenuhnya benar. Enum harus ditutup. Saya sangat menyukai ide "menyusun" enum karena menurut saya ini adalah inti dari masalah yang kami inginkan di sini .

Saya pikir notasi ini merangkum konsep "menjahit bersama" dua enum terpisah dengan cukup elegan:

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

Jadi pertimbangkan bahwa pengunduran diri saya menggunakan istilah extends


Komentar atas jawaban @masak atas pertanyaan @JeffreyMercado :

  • Saya berasumsi kita hanya ingin mengizinkan perluasan enum dengan enum "tipe yang sama" (numeric extends numeric, string extends string). Enum heterogen secara teknis didukung jadi saya kira kita harus mempertahankan dukungan itu.

Saya berasumsi begitu juga. Enum tipe campuran yang dihasilkan tampaknya tidak terlalu berguna.

Meskipun saya setuju bahwa itu tidak berguna, kami mungkin HARUS menyimpan dukungan heterogen untuk enum di sini. Saya pikir peringatan linter akan berguna di sini, tetapi saya tidak berpikir TS harus menghalangi itu. Saya dapat memikirkan kasus penggunaan yang dibuat-buat yaitu, saya sedang membangun enum untuk interaksi dengan API yang dirancang sangat buruk yang mengambil flag yang merupakan campuran angka dan string. Dibikin, saya tahu, tapi karena itu diperbolehkan di tempat lain, saya tidak berpikir kita harus melarangnya di sini.

Mungkin hanya dorongan kuat untuk tidak?

  • Haruskah kita mengizinkan perluasan dari beberapa enum? Haruskah mereka semua memiliki nilai yang saling eksklusif? Atau akankah kita mengizinkan nilai yang tumpang tindih? Prioritas berdasarkan urutan leksikal?

Karena menyalin semantik yang sedang kita bicarakan, menduplikasi banyak enum tampaknya "lebih baik" daripada Multiple Inheritance la C++. Saya tidak langsung melihat masalah dengan itu, terutama jika kita terus membangun analogi penyebaran objek: let newEnum = { ...enumA, ...enumB };

100% setuju

  • Haruskah semua anggota memiliki nilai yang saling eksklusif?

Hal yang konservatif adalah mengatakan "ya". Sekali lagi, analogi penyebaran objek memberi kita semantik alternatif: yang terakhir menang.

Aku robek di sini. Sementara saya setuju itu "praktik terbaik" untuk menegakkan eksklusivitas nilai timbal balik, apakah itu benar? Ini secara langsung bertentangan dengan semantik penyebaran yang umum dikenal. Di satu sisi, saya menyukai gagasan untuk menegakkan nilai-nilai yang saling eksklusif, di sisi lain, itu mematahkan banyak asumsi tentang bagaimana semantik yang tersebar seharusnya bekerja. Apakah ada kerugian untuk mengikuti aturan penyebaran normal dengan "yang terakhir menang"? Sepertinya lebih mudah dalam implementasi (karena objek yang mendasarinya hanyalah peta). Tetapi tampaknya juga selaras dengan harapan umum. Saya condong ke arah yang kurang mengejutkan.

Mungkin juga ada contoh bagus untuk ingin mengganti nilai (walaupun saya tidak tahu apa itu).

Tetapi setelah dipikir-pikir, baik di bawah "tidak ada tabrakan" dan "yang terakhir menang", saya merasa aneh bahkan ingin menempatkan deklarasi anggota enum sebelum enum menyebar dalam daftar. Seperti, maksud apa yang dikomunikasikan dengan melakukan itu? Spreadnya sedikit seperti "impor", dan ini secara konvensional berada di atas.

Yah itu tergantung, jika kita mengikuti semantik yang tersebar, maka tidak masalah apa urutannya. Sejujurnya, bahkan jika kita menegakkan nilai-nilai yang saling eksklusif, urutannya tidak akan menjadi masalah, kan? Tabrakan akan menjadi kesalahan pada saat itu, terlepas dari urutannya.

  • Saya berasumsi nilai numerik implisit akan berlanjut 1 setelah nilai maksimal enum numerik yang diperluas.

Mungkin hal konservatif yang harus dilakukan adalah meminta nilai eksplisit untuk anggota pertama setelah spread.

Saya setuju. Jika Anda menyebarkan enum, TS seharusnya hanya menerapkan nilai eksplisit untuk anggota tambahan.

@julian-sf

Jadi pertimbangkan bahwa pengunduran diri saya menggunakan istilah itu diperpanjang

:+1: Masyarakat untuk Pelestarian Jenis Sum bersorak dari pinggir lapangan.

Tetapi setelah dipikir-pikir, baik di bawah "tidak ada tabrakan" dan "yang terakhir menang", saya merasa aneh bahkan ingin menempatkan deklarasi anggota enum sebelum enum menyebar dalam daftar. Seperti, maksud apa yang dikomunikasikan dengan melakukan itu? Spreadnya sedikit seperti "impor", dan ini secara konvensional berada di atas.

Yah itu tergantung, jika kita mengikuti semantik yang tersebar, maka tidak masalah apa urutannya. Sejujurnya, bahkan jika kita menegakkan nilai-nilai yang saling eksklusif, urutannya tidak akan menjadi masalah, kan? Tabrakan akan menjadi kesalahan pada saat itu, terlepas dari urutannya.

Saya mengatakan "tidak ada alasan yang baik untuk menempatkan spread setelah deklarasi anggota biasa"; Anda mengatakan "di bawah batasan yang sesuai, menempatkannya sebelum atau sesudah tidak ada bedanya". Kedua hal ini bisa benar pada saat yang bersamaan.

Perbedaan utama dalam hasil tampaknya jatuh pada spektrum yang mengizinkan atau melarang spread sebelum anggota normal. Itu bisa secara sintaksis dianulir; itu bisa menghasilkan peringatan linter; atau bisa jadi baik-baik saja dalam segala hal. Jika urutannya tidak membuat perbedaan semantik, maka turunlah untuk membuat fitur penyebaran enum mengikuti prinsip Kejutan Terkecil , mudah digunakan, dan mudah diajarkan/dijelaskan.

Menggunakan operator spread termasuk dalam penggunaan salinan dangkal yang lebih luas di seluruh JS dan TypeScript. Ini tentu saja metode yang lebih banyak digunakan dan lebih mudah dipahami daripada menggunakan extends , yang menyiratkan hubungan langsung. Membuat enum melalui komposisi akan menjadi solusi yang lebih mudah untuk dikonsumsi.

Beberapa saran untuk mengatasi, meskipun valid dan dapat digunakan, menambahkan lebih banyak kode boilerplate untuk mencapai hasil yang diinginkan yang sama. Mengingat sifat enum yang final dan tidak dapat diubah, membuat enum tambahan melalui komposisi akan diinginkan, untuk mempertahankan properti yang konsisten dengan bahasa lain.

Sayang sekali bahwa 3 tahun percakapan ini masih berlangsung.

@ jmitchell38488 Saya akan memberikan suka pada komentar Anda, tetapi kalimat terakhir Anda mengubah pikiran saya. Ini adalah diskusi yang diperlukan, karena solusi yang diusulkan akan berhasil, tetapi ini juga menyiratkan kemungkinan untuk memperluas kelas dan antarmuka dengan cara ini. Ini adalah perubahan besar yang mungkin membuat beberapa programmer bahasa mirip c++ ​​takut menggunakan TypeScript, karena pada dasarnya Anda memiliki 2 cara untuk melakukan hal yang sama ( class A extends B dan class A { ...(class B {}) } ). Saya pikir, kedua cara dapat didukung, tetapi kemudian kita membutuhkan extend untuk enum juga untuk konsistensi.

@masak wdyt? ^

@sdwvit Saya tidak bermaksud mengubah perilaku untuk membuat kelas dan antarmuka, saya berbicara secara khusus tentang enum dan membuatnya melalui komposisi. Mereka adalah tipe akhir yang tidak dapat diubah, jadi kita seharusnya tidak dapat memperluas dengan cara pewarisan yang khas.

Mengingat sifat JS dan nilai transpilasi akhir, tidak ada alasan mengapa komposisi tidak dapat dicapai. Tentu akan membuat bekerja dengan enum lebih menarik.

@masak wdyt? ^

Hm. Saya pikir konsistensi bahasa adalah tujuan yang terpuji, dan oleh karena itu tidak _apriori_ salah untuk meminta fitur ... serupa di kelas dan antarmuka. Tapi saya pikir kasingnya lebih lemah atau tidak ada di sana, dan karena dua alasan: (a) kelas dan antarmuka sudah memiliki mekanisme ekstensi, dan menambahkan yang kedua memberikan sedikit nilai tambahan (sedangkan menyediakan satu untuk enum akan mencakup kasus penggunaan yang orang telah kembali ke masalah ini selama bertahun-tahun); (b) menambahkan sintaks dan semantik baru ke kelas memiliki bar persetujuan yang jauh lebih tinggi, karena kelas dalam beberapa hal berasal dari spesifikasi EcmaScript. (TypeScript lebih tua dari ES6, tetapi ada upaya aktif untuk membuat yang pertama tetap dekat dengan yang terakhir. Itu termasuk tidak memperkenalkan fitur tambahan di atas.)

Saya pikir utas ini telah dibuka untuk waktu yang lama hanya karena ini mewakili fitur yang layak yang akan mencakup kasus penggunaan yang sebenarnya, tetapi belum ada PR yang dibuat untuk itu. Membuat PR seperti itu membutuhkan waktu dan usaha, lebih dari sekadar mengatakan Anda menginginkan fitur tersebut. :mengedip:

Apakah ada yang mengerjakan fitur ini?

Apakah ada yang mengerjakan fitur ini?

Dugaan saya tidak, karena kita bahkan belum menyelesaikan diskusi tentang itu, haha!

Saya merasa kita telah mendekati konsensus tentang seperti apa ini nantinya. Karena ini akan menjadi tambahan bahasa, mungkin diperlukan "juara" untuk mendorong proposal ini ke depan. Saya pikir seseorang dari tim TS perlu masuk dan memindahkan masalah dari "Menunggu umpan balik" menjadi "Menunggu proposal" (atau yang serupa).

Saya sedang mengerjakan prototipenya. Meskipun saya belum terlalu jauh karena kurangnya waktu dan ketidaktahuan dengan struktur kode. Saya ingin melihat ini melalui dan jika tidak ada gerakan lain, saya akan melanjutkan ketika saya bisa.

Juga akan menyukai fitur ini. 37 bulan telah berlalu, semoga segera ada kemajuan

Catatan dari pertemuan terakhir:

  • Kami menyukai sintaks spread, karena extends menyiratkan subtipe, sedangkan "memperluas" enum membuat supertipe.

    enum BasicEvents {
     Start = "Start",
     Finish = "Finish"
    }
    enum AdvEvents {
     ...BasicEvents,
     Pause = "Pause",
     Resume = "Resume"
    }
    
  • Pikirannya adalah AdvEvents.Start akan diselesaikan dengan identitas tipe yang sama dengan BasicEvents.Start . Ini memiliki implikasi bahwa jenis BasicEvents.Start dan AdvEvents.Start akan dapat ditetapkan satu sama lain, dan jenis BasicEvents akan dapat ditetapkan ke AdvEvents . Mudah-mudahan ini masuk akal secara intuitif, tetapi penting untuk dicatat bahwa ini berarti spread bukan hanya pintasan sintaksis—jika kita memperluas spread menjadi seperti apa artinya:

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

    ini memiliki perilaku yang berbeda—di sini, BasicEvents.Start dan AdvEvents.Start _tidak_ dapat ditetapkan satu sama lain, karena kualitas string enum yang buram.

    Konsekuensi kecil lainnya dari implementasi ini adalah bahwa AdvEvents.Start mungkin akan diserialisasi sebagai BasicEvents.Start dalam info cepat dan emisi deklarasi (setidaknya jika tidak ada konteks sintaksis untuk menautkan anggota ke AdvEvents —mengarahkan kursor ke ekspresi literal AdvEvents.Start masuk akal dapat menghasilkan info cepat yang mengatakan AdvEvents.Start , tetapi mungkin lebih jelas untuk menampilkan BasicEvents.Start ).

  • Ini hanya akan diizinkan dalam string enum.

Saya ingin mencoba yang satu ini.

Untuk memperjelas: Ini tidak disetujui untuk diterapkan.

Ada dua kemungkinan perilaku di sini, dan keduanya tampak buruk.

Opsi 1: Ini Sebenarnya Gula

Jika spread benar-benar berarti sama dengan menyalin anggota enum yang tersebar, maka akan ada kejutan besar ketika orang mencoba menggunakan nilai enum yang diperluas seolah-olah itu adalah nilai enum yang diperluas dan itu tidak berhasil.

Opsi 2: Ini Sebenarnya Bukan Gula

Opsi yang lebih disukai adalah bahwa spread berfungsi lebih seperti saran tipe serikat @aj-r di dekat bagian atas utas. Jika itu adalah perilaku yang sudah diinginkan orang, maka opsi yang ada di atas meja tampaknya lebih disukai demi pemahaman. Kalau tidak, kami membuat jenis string enum baru yang tidak berperilaku seperti string enum lainnya, yang aneh dan tampaknya merusak "kesederhanaan" saran di sini.

Perilaku apa yang diinginkan orang, dan mengapa?

Saya tidak ingin opsi 1, karena itu mengarah pada kejutan besar.

Saya menginginkan opsi 2, tetapi saya ingin memiliki dukungan sintaks yang cukup untuk mengatasi kerugian yang disebutkan @aj-r, sehingga saya dapat menulis let e: Events = Events.Pause; dari contohnya. Kelemahannya tidak buruk, tetapi ini adalah tempat di mana enum yang diperluas tidak dapat menyembunyikan implementasinya; jadi agak kotor.

Saya juga berpikir opsi 1 adalah ide yang buruk. Saya mencari referensi untuk masalah ini di perusahaan saya dan menemukan dua ulasan kode di mana itu ditautkan, dan dalam kedua kasus (dan dalam pengalaman pribadi saya), ada kebutuhan yang jelas untuk elemen enum yang lebih kecil untuk dapat dialihkan ke tipe enum yang lebih besar . Saya terutama khawatir tentang satu orang yang memperkenalkan ... berpikir itu berperilaku seperti opsi 2, dan kemudian orang berikutnya menjadi sangat bingung (atau beralih ke peretasan seperti as unknown as Events.Pause ) ketika kasus yang lebih kompleks tidak berhasil.

Sudah ada banyak cara untuk mencoba mendapatkan perilaku opsi 2: banyak cuplikan di utas ini, ditambah berbagai pendekatan yang melibatkan penyatuan literal string. Kekhawatiran besar saya dengan menerapkan opsi 1 adalah bahwa ia secara efektif memperkenalkan cara lain yang salah untuk mendapatkan opsi 2, dan dengan demikian menyebabkan lebih banyak pemecahan masalah dan frustrasi bagi orang yang mempelajari bagian TypeScript ini.

Perilaku apa yang diinginkan orang, dan mengapa?

Karena kode berbicara lebih keras daripada kata-kata, dan menggunakan contoh dari 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

Contoh ini menampilkan opsi 2, bukan? Jika demikian, maka saya sangat menginginkan opsi 2.

Kalau tidak, kami membuat jenis string enum baru yang tidak berperilaku seperti string enum lainnya, yang aneh dan tampaknya merusak "kesederhanaan" saran di sini

Saya setuju bahwa opsi 2 sedikit meresahkan dan mungkin lebih baik untuk mundur dan memikirkan alternatif. Berikut adalah eksplorasi bagaimana hal itu dapat dilakukan tanpa menambahkan sintaks atau perilaku baru ke enum hari ini:

Saya pikir saran saya di https://github.com/microsoft/TypeScript/issues/17592#issuecomment -449440944 mendapatkan yang paling dekat hari ini dan mungkin sesuatu untuk dikerjakan:

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

Saya melihat dua masalah utama dengan pendekatan itu:

  • Sangat membingungkan bagi seseorang yang mempelajari TypeScript; jika Anda tidak memiliki pemahaman yang kuat tentang ruang tipe vs ruang nilai, sepertinya itu menimpa tipe dengan konstanta.
  • Itu tidak lengkap karena tidak memungkinkan Anda untuk menggunakan Events.Pause sebagai tipe.

Saya sebelumnya menyarankan https://github.com/microsoft/TypeScript/issues/29130 yang akan (antara lain) membahas poin-poin kedua, dan saya pikir itu masih bisa berharga, meskipun itu pasti akan menambah banyak kehalusan pada bagaimana nama bekerja.

Satu ide sintaks yang menurut saya akan membahas kedua poin adalah sintaks alternatif const yang juga mendeklarasikan tipe:

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

Ini akan lebih dekat ke dunia di mana enum tidak diperlukan sama sekali. (Bagi saya, enum selalu merasa seperti mereka bertentangan dengan tujuan desain TS karena mereka adalah sintaks tingkat ekspresi dengan perilaku emisi non-sepele dan nominal secara default.) Sintaks deklarasi const type juga memungkinkan Anda buat string enum yang diketik secara struktural seperti ini:

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

Memang, cara ini perlu bekerja adalah bahwa tipe BasicEvents adalah singkatan untuk typeof BasicEvents[keyof typeof BasicEvents] , yang mungkin terlalu fokus untuk sintaks yang diberi nama umum seperti const type . Mungkin kata kunci yang berbeda akan lebih baik. Sayang const enum sudah diambil .

Dari sana, saya pikir satu-satunya celah antara enum (string) dan literal objek adalah pengetikan nominal. Itu mungkin bisa diselesaikan dengan menggunakan sintaks seperti as unique atau const unique type yang pada dasarnya memilih perilaku pengetikan nominal seperti enum untuk tipe objek ini, dan idealnya juga untuk konstanta string biasa. Itu juga akan memberikan pilihan yang jelas antara opsi 1 dan opsi 2 ketika mendefinisikan Events : Anda menggunakan unique ketika Anda bermaksud Events menjadi tipe yang benar-benar berbeda (opsi 1), dan Anda menghilangkan unique bila Anda menginginkan penetapan antara Events dan BasicEvents (opsi 2).

Dengan const type dan const unique type , akan ada cara untuk menyatukan enum yang ada dengan rapi, dan juga cara untuk mengekspresikan enum sebagai kombinasi alami fitur TS daripada hanya sekali.

Apa yang sedang terjadi disini? 😅.

wow dari 2017 , umpan balik apa lagi yang Anda butuhkan?

apa lagi umpan balik yang Anda butuhkan?

Disini??? Kami tidak hanya menerapkan saran karena sudah tua!

apa lagi umpan balik yang Anda butuhkan?

Disini??? Kami tidak hanya menerapkan saran karena sudah tua!

Ya. Saya tidak yakin ke arah mana itu.

Membaca lagi dan melihat juga #40998 Saya pikir itu cara terbaik ... emun adalah objek dan saya pikir penyebarannya lebih mudah untuk menggabungkan/memperpanjang enum.

Saya tidak berpikir saya cukup memenuhi syarat untuk menawarkan pendapat saya tentang desain bahasa, tetapi saya pikir saya dapat memberikan umpan balik sebagai pengembang biasa.

Saya telah menemukan masalah ini beberapa minggu sebelumnya dalam proyek nyata di mana saya ingin menggunakan fitur ini. Saya akhirnya menggunakan pendekatan @alangpierce . Sayangnya, karena tanggung jawab saya kepada majikan saya, saya tidak dapat membagikan kodenya di sini, tetapi berikut adalah beberapa poinnya:

  1. Deklarasi berulang (baik tipe dan const untuk enum baru) bukan masalah besar dan tidak banyak merusak keterbacaan.
  2. Dalam kasus saya, enum mewakili tindakan yang berbeda untuk algoritme tertentu, dan ada rangkaian tindakan yang berbeda untuk situasi yang berbeda dalam algoritme ini. Menggunakan hierarki tipe memungkinkan saya untuk memverifikasi bahwa tindakan tertentu tidak dapat terjadi dalam waktu kompilasi: ini adalah inti dari semuanya dan ternyata cukup berguna.
  3. Kode yang saya tulis adalah perpustakaan internal, dan sementara perbedaan antara tipe enum yang berbeda penting di dalam perpustakaan, itu tidak masalah bagi pengguna luar perpustakaan ini. Dengan menggunakan pendekatan ini, saya hanya dapat mengekspor satu tipe yang merupakan tipe penjumlahan dari semua enum yang berbeda di dalamnya, dan menyembunyikan detail implementasi.
  4. Sayangnya, saya tidak dapat menemukan cara idiomatik dan mudah dibaca untuk mengurai nilai dari tipe jumlah secara otomatis dari deklarasi tipe. (Dalam kasus saya, langkah-langkah algoritme berbeda yang saya sebutkan diambil dari database SQL saat runtime). Itu bukan masalah besar (menulis kode parsing secara manual cukup mudah), tetapi alangkah baiknya jika implementasi enum inheritance memperhatikan masalah ini.

Secara keseluruhan, saya pikir banyak proyek nyata dengan logika bisnis yang membosankan akan mendapat banyak manfaat dari fitur ini. Memisahkan enum menjadi subtipe yang berbeda akan memungkinkan sistem tipe untuk memeriksa banyak invarian yang sekarang diperiksa oleh unit test, dan membuat nilai yang salah tidak dapat diwakili oleh sistem tipe selalu merupakan hal yang baik.

Hai,

Biarkan saya menambahkan dua sen saya di sini

konteks saya

Saya memiliki API, dengan dokumentasi OpenApi yang dihasilkan dengan tsoa .

Salah satu model saya memiliki status yang ditentukan seperti ini:

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

Saya memiliki metode setStatus yang mengambil subset dari status ini. Karena fitur ini tidak tersedia, saya mempertimbangkan untuk menduplikasi enum seperti itu:

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

Jadi metode saya dijelaskan seperti ini:

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

dengan kode itu, saya mendapatkan kesalahan ini:

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.

yang akan saya lakukan untuk saat ini, tetapi penasaran dan mulai mencari repositori ini, ketika saya menemukan ini.
Setelah menggulir ke bawah, saya tidak menemukan siapa pun (atau mungkin saya melewatkan sesuatu) yang menyarankan kasus penggunaan ini.

Dalam kasus penggunaan saya, saya tidak ingin "memperpanjang" dari enum karena tidak ada alasan EntityStatus akan menjadi perpanjangan dari RequestedEntityStatus. Saya lebih suka dapat "Memilih" dari enum yang lebih umum.

Lamaran ku

Saya menemukan operator spread lebih baik daripada proposal ekstensi, tetapi saya ingin melangkah lebih jauh dan menyarankan ini:

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

Lebih umum, ini harus bekerja:

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

Dengan kata lain

Saya ingin melonggarkan batasan pada tipe enum untuk berperilaku lebih seperti serikat pekerja ( 'a' | 'b' ) daripada struktur buram.

Dengan menambahkan kemampuan tersebut ke kompiler, dua enum independen dengan nilai yang sama dapat ditugaskan satu sama lain dengan aturan yang sama seperti serikat pekerja:
Mengingat enum berikut:

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

Dan tiga variabel a:A , b:B dan c:C

  • c = a seharusnya berfungsi, karena A hanyalah bagian dari C, jadi setiap nilai A adalah nilai C yang valid
  • c = b seharusnya berfungsi, karena B hanya 'a' | 'c' yang keduanya merupakan nilai C yang valid
  • b = a dapat bekerja jika a diketahui berbeda dari 'b' (yang akan sama dengan jenis 'a' saja)
  • a = b , juga, dapat berfungsi jika b diketahui berbeda dari 'c'
  • b = c dapat bekerja jika c diketahui berbeda dari 'b' (yang akan sama dengan 'a'|'c' , yang persis seperti B)

atau mungkin kita perlu pemeran eksplisit di sisi kanan, untuk perbandingan kesetaraan?

Tentang konflik anggota enum

Saya bukan penggemar aturan "kemenangan terakhir", meskipun itu terasa alami dengan operator spread.
Saya akan mengatakan bahwa, kompiler harus mengembalikan kesalahan jika "kunci" atau "nilai" dari enum diduplikasi, kecuali jika kunci dan nilainya identik:

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'

Penutupan

Saya menemukan proposal ini cukup fleksibel dan alami untuk digunakan oleh pengembang TS, sambil memungkinkan lebih banyak keamanan jenis (dibandingkan dengan as unknown as T ) tanpa benar-benar mengubah apa itu enum. Itu hanya memperkenalkan cara baru untuk menambahkan anggota ke enum, dan cara baru untuk membandingkan enum satu sama lain.
Bagaimana menurutmu? Apakah saya melewatkan beberapa masalah arsitektur bahasa yang jelas yang membuat fitur ini tidak dapat dicapai?

Apakah halaman ini membantu?
0 / 5 - 0 peringkat