Bagaimana cara kerja varian enum yang berbeda di TypeScript?

116

TypeScript memiliki banyak cara berbeda untuk menentukan enum:

enum Alpha { X, Y, Z }
const enum Beta { X, Y, Z }
declare enum Gamma { X, Y, Z }
declare const enum Delta { X, Y, Z }

Jika saya mencoba menggunakan nilai dari Gammasaat runtime, saya mendapatkan kesalahan karena Gammatidak ditentukan, tetapi bukan itu masalahnya Deltaatau Alpha? Apa maksud constatau declaremaksud deklarasi di sini?

Ada juga tanda preserveConstEnumskompilator - bagaimana ini berinteraksi dengan ini?

Ryan Cavanaugh
sumber
1
Saya baru saja menulis artikel tentang ini , meskipun ini lebih berkaitan dengan membandingkan const ke non const enums
joelmdev

Jawaban:

247

Ada empat aspek berbeda untuk enum di TypeScript yang perlu Anda waspadai. Pertama, beberapa definisi:

"objek pencarian"

Jika Anda menulis enum ini:

enum Foo { X, Y }

TypeScript akan memancarkan objek berikut:

var Foo;
(function (Foo) {
    Foo[Foo["X"] = 0] = "X";
    Foo[Foo["Y"] = 1] = "Y";
})(Foo || (Foo = {}));

Saya akan merujuk ini sebagai objek pencarian . Tujuannya ada dua: berfungsi sebagai pemetaan dari string ke angka , misalnya saat menulis Foo.Xatau Foo['X'], dan berfungsi sebagai pemetaan dari angka ke string . Pemetaan terbalik itu berguna untuk tujuan debugging atau logging - Anda akan sering memiliki nilai 0atau 1dan ingin mendapatkan string yang sesuai "X"atau "Y".

"menyatakan" atau " ambient "

Dalam TypeScript, Anda dapat "mendeklarasikan" hal-hal yang harus diketahui oleh compiler, tetapi tidak benar-benar mengeluarkan kode. Ini berguna ketika Anda memiliki pustaka seperti jQuery yang mendefinisikan beberapa objek (misalnya $) yang Anda inginkan informasinya diketik, tetapi tidak memerlukan kode apa pun yang dibuat oleh kompiler. Spesifikasi dan dokumentasi lainnya mengacu pada deklarasi yang dibuat dengan cara ini sebagai konteks "ambient"; penting untuk dicatat bahwa semua deklarasi dalam .d.tsfile bersifat "ambient" (baik membutuhkan declarepengubah eksplisit atau memilikinya secara implisit, tergantung pada jenis deklarasi).

"inlining"

Untuk alasan performa dan ukuran kode, sering kali lebih baik jika referensi ke anggota enum diganti dengan padanan numeriknya saat dikompilasi:

enum Foo { X = 4 }
var y = Foo.X; // emits "var y = 4";

Spek menyebut substitusi ini , saya akan menyebutnya sebaris karena terdengar lebih keren. Terkadang Anda tidak ingin anggota enum menjadi sebaris, misalnya karena nilai enum mungkin berubah di versi API yang akan datang.


Enums, bagaimana cara kerjanya?

Mari kita uraikan ini dengan setiap aspek enum. Sayangnya, masing-masing dari empat bagian ini akan mereferensikan istilah-istilah dari yang lainnya, jadi Anda mungkin perlu membaca keseluruhan ini lebih dari sekali.

dihitung vs tidak dihitung (konstan)

Anggota enum dapat dihitung atau tidak. Spec memanggil anggota yang tidak dihitung konstan , tetapi saya akan menyebutnya non-dihitung untuk menghindari kebingungan dengan const .

Sebuah dihitung anggota enum adalah salah satu yang nilainya tidak diketahui pada saat kompilasi. Referensi ke anggota yang dihitung tidak dapat disisipkan, tentu saja. Sebaliknya, non-computed anggota enum sekali yang nilainya adalah dikenal pada saat kompilasi. Referensi ke anggota yang tidak dihitung selalu sebaris.

Anggota enum mana yang dihitung dan mana yang tidak dihitung? Pertama, semua anggota constenum adalah konstan (yaitu tidak dihitung), seperti namanya. Untuk enum non-konst, itu tergantung pada apakah Anda melihat enum ambien (deklarasikan) atau enum non-ambien.

Anggota a declare enum(yaitu enum ambien) konstan jika dan hanya jika memiliki penginisialisasi. Jika tidak, itu dihitung. Perhatikan bahwa dalam a declare enum, hanya penginisialisasi numerik yang diperbolehkan. Contoh:

declare enum Foo {
    X, // Computed
    Y = 2, // Non-computed
    Z, // Computed! Not 3! Careful!
    Q = 1 + 1 // Error
}

Akhirnya, anggota non-deklarasikan non-const enum selalu dianggap dihitung. Namun, ekspresi inisialisasi mereka dikurangi menjadi konstanta jika dapat dihitung pada waktu kompilasi. Artinya, anggota enum non-konst tidak pernah sebaris (perilaku ini diubah di TypeScript 1.5, lihat "Perubahan di TypeScript" di bagian bawah)

const vs non-const

const

Deklarasi enum dapat memiliki constpengubah. Jika enum adalah const, semua referensi ke anggotanya disisipkan.

const enum Foo { A = 4 }
var x = Foo.A; // emitted as "var x = 4;", always

const enums tidak menghasilkan objek pencarian saat dikompilasi. Karena alasan ini, adalah kesalahan untuk merujuk Foopada kode di atas kecuali sebagai bagian dari referensi anggota. Tidak ada Fooobjek yang akan hadir saat runtime.

non-const

Jika deklarasi enum tidak memiliki constpengubah, referensi ke anggotanya hanya sebaris jika anggota tersebut tidak dihitung. Enum non-const, non-deklarasikan akan menghasilkan objek pencarian.

menyatakan (ambient) vs non-deklarasikan

Kata pengantar yang penting adalah bahwa declaredi TypeScript memiliki arti yang sangat spesifik: Objek ini ada di tempat lain . Ini untuk mendeskripsikan objek yang ada . Menggunakan declareuntuk mendefinisikan objek yang sebenarnya tidak ada dapat memiliki konsekuensi yang buruk; kita akan menjelajahinya nanti.

menyatakan

A declare enumtidak akan memancarkan objek pencarian. Referensi ke anggotanya menjadi inline jika anggota tersebut dihitung (lihat di atas di computed vs non-computed).

Sangat penting untuk dicatat bahwa bentuk-bentuk lain dari referensi untuk declare enum yang diperbolehkan, misalnya kode ini tidak error kompilasi tapi akan gagal saat runtime:

// Note: Assume no other file has actually created a Foo var at runtime
declare enum Foo { Bar } 
var s = 'Bar';
var b = Foo[s]; // Fails

Kesalahan ini termasuk dalam kategori "Jangan berbohong pada kompiler". Jika Anda tidak memiliki objek bernama Foosaat runtime, jangan menulis declare enum Foo!

A declare const enumtidak berbeda dari a const enum, kecuali dalam kasus --preserveConstEnums (lihat di bawah).

non-deklarasikan

Enum yang tidak mendeklarasikan menghasilkan objek pencarian jika tidak const. Sebaris dijelaskan di atas.

--preserveConstEnums

Bendera ini memiliki satu efek: non-deklarasikan enum const akan memancarkan objek pencarian. Sebaris tidak terpengaruh. Ini berguna untuk debugging.


Kesalahan Umum

Kesalahan paling umum adalah menggunakan a declare enumwhen a regular enumatau const enumakan lebih tepat. Bentuk umum adalah ini:

module MyModule {
    // Claiming this enum exists with 'declare', but it doesn't...
    export declare enum Lies {
        Foo = 0,
        Bar = 1     
    }
    var x = Lies.Foo; // Depend on inlining
}

module SomeOtherCode {
    // x ends up as 'undefined' at runtime
    import x = MyModule.Lies;

    // Try to use lookup object, which ought to exist
    // runtime error, canot read property 0 of undefined
    console.log(x[x.Foo]);
}

Ingat aturan emas: Jangan pernah declarehal-hal yang sebenarnya tidak ada . Gunakan const enumjika Anda selalu ingin sebaris, atau enumjika Anda menginginkan objek pencarian.


Perubahan TypeScript

Antara TypeScript 1.4 dan 1.5, ada perubahan dalam perilaku (lihat https://github.com/Microsoft/TypeScript/issues/2183 ) untuk membuat semua anggota non-deklarasikan enum non-konst diperlakukan sebagai dihitung, bahkan jika mereka secara eksplisit diinisialisasi dengan literal. Ini "memisahkan bayi", sehingga membuat perilaku inlining lebih dapat diprediksi dan lebih jelas memisahkan konsep const enumdari biasa enum. Sebelum perubahan ini, anggota non-konstanta enum yang tidak dihitung dimasukkan lebih agresif.

Ryan Cavanaugh
sumber
6
Jawaban yang sangat luar biasa. Itu membereskan banyak hal bagi saya, bukan hanya enum.
Clark
1
Saya berharap saya dapat memilih Anda lebih dari sekali ... tidak tahu tentang perubahan yang menghancurkan itu. Dalam pembuatan versi semantik yang tepat, ini dapat dianggap sebagai peningkatan ke versi utama: - /
mfeineis
Perbandingan yang sangat membantu dari berbagai enumjenis, terima kasih!
Marius Schulz
@Ryan ini sangat membantu, terima kasih! Sekarang kita hanya membutuhkan Web Essentials 2015 untuk menghasilkan constjenis enum yang tepat untuk dideklarasikan.
styfle
19
Jawaban ini tampaknya sangat detail menjelaskan situasi di 1.4, dan kemudian di bagian paling akhir dikatakan "tetapi 1,5 mengubah semua itu dan sekarang jauh lebih sederhana." Dengan asumsi saya memahami berbagai hal dengan benar, organisasi ini akan menjadi semakin tidak tepat karena jawaban ini semakin tua: Saya sangat menyarankan untuk menempatkan situasi saat ini yang lebih sederhana terlebih dahulu , dan hanya setelah itu mengatakan "tetapi jika Anda menggunakan 1.4 atau sebelumnya, semuanya sedikit lebih rumit. "
KRyan
33

Ada beberapa hal yang terjadi di sini. Mari kita lanjutkan kasus per kasus.

enum

enum Cheese { Brie, Cheddar }

Pertama, enum tua biasa. Saat dikompilasi ke JavaScript, ini akan mengeluarkan tabel pencarian.

Tabel pemeta terlihat seperti ini:

var Cheese;
(function (Cheese) {
    Cheese[Cheese["Brie"] = 0] = "Brie";
    Cheese[Cheese["Cheddar"] = 1] = "Cheddar";
})(Cheese || (Cheese = {}));

Kemudian ketika Anda memiliki Cheese.BrieTypeScript, itu memancarkan Cheese.Briedalam JavaScript yang mengevaluasi 0. Cheese[0]emitCheese[0] dan benar-benar mengevaluasi ke "Brie".

const enum

const enum Bread { Rye, Wheat }

Tidak ada kode yang benar-benar dikeluarkan untuk ini! Nilainya segaris. Yang berikut ini mengeluarkan nilai 0 itu sendiri di JavaScript:

Bread.Rye
Bread['Rye']

const enums 'inlining mungkin berguna untuk alasan kinerja.

Tapi bagaimana dengan Bread[0]? Ini akan error saat runtime dan compiler Anda harus menangkapnya. Tidak ada tabel pemeta dan kompilator tidak sebaris di sini.

Perhatikan bahwa dalam kasus di atas, flag --preserveConstEnums akan menyebabkan Bread mengeluarkan tabel pencarian. Nilainya akan tetap inline.

nyatakan enum

Seperti penggunaan lainnya declare, declaretidak mengeluarkan kode dan mengharapkan Anda untuk menentukan kode sebenarnya di tempat lain. Ini tidak memancarkan tabel pencarian:

declare enum Wine { Red, Wine }

Wine.Red memancarkan Wine.Red dalam JavaScript, tetapi tidak akan ada tabel pencarian Wine untuk referensi sehingga ini adalah kesalahan kecuali Anda telah menetapkannya di tempat lain.

deklarasikan const enum

Ini tidak memancarkan tabel pencarian:

declare const enum Fruit { Apple, Pear }

Tapi itu sebaris! Fruit.Applememancarkan 0. Tapi sekali lagi Fruit[0]akan error saat runtime karena tidak inline dan tidak ada tabel lookup.

Saya telah menulis ini di taman bermain ini . Saya sarankan bermain di sana untuk memahami TypeScript mana yang memancarkan JavaScript.

Kat
sumber
1
Saya sarankan memperbarui jawaban ini: Pada Typecript 3.3.3, Bread[0]muncul kesalahan compiler: "Anggota const enum hanya dapat diakses menggunakan string literal."
chharvey
1
Hm ... apakah itu berbeda dari jawaban yang tertulis? "Tapi bagaimana dengan Bread [0]? Ini akan error saat runtime dan kompilator Anda harus menangkapnya. Tidak ada tabel pencarian dan kompilator tidak sebaris di sini."
Kat