never adalah tipe tanpa nilai. Fungsi dengan tipe kembalian never tidak pernah selesai secara normal: ia melempar error atau berjalan selamanya. Variabel bertipe never menandai kode yang tidak mungkin berjalan, dan itulah yang membuat exhaustive check bisa dilakukan.
return fail(...) lolos kompilasi di fungsi yang mengembalikan number karena never bisa di-assign ke setiap tipe: nilai bertipe never tidak akan pernah benar-benar sampai.
Fungsi yang Tidak Pernah Selesai
Ada dua jenis fungsi yang tidak pernah mencapai akhirnya: yang selalu melempar error, dan yang punya perulangan tanpa akhir. Compiler memeriksa klaim ini. Fungsi yang dianotasi never tetapi akhirnya bisa dijangkau adalah error TS2534, A function returning 'never' cannot have a reachable end point.
function fail(message: string): never {
throw new Error(message);
}
function runForever(): never {
while (true) {
// poll, serve requests...
}
}
Inferensinya berbeda tergantung sintaks. Deklarasi fungsi yang hanya melempar error disimpulkan mengembalikan void, sedangkan arrow function atau function expression yang hanya melempar error disimpulkan sebagai never:
function f1() { throw new Error("x"); } // () => void
const f2 = () => { throw new Error("x"); }; // () => never
Narrowing hanya memperlakukan sebuah pemanggilan sebagai jalan buntu kalau nama yang dipanggil punya tipe eksplisit yang mengembalikan never: deklarasi fungsi yang dianotasi : never, seperti fail di atas, atau variabel dengan anotasi tipe, const fail: (m: string) => never = (m) => { throw new Error(m); }. never hasil inferensi tidak dihitung, begitu juga const fail = (m: string): never => ..., yang hanya arrow function-nya yang dianotasi, bukan variabelnya.
never vs void
void | never | |
|---|---|---|
| Fungsi selesai | ya | tidak (melempar error atau berulang selamanya) |
| Nilai saat runtime | undefined | tidak ada: pemanggilannya tidak pernah menghasilkan nilai |
| Kode setelah pemanggilan | bisa dijangkau | tidak bisa dijangkau |
| Bisa di-assign ke tipe lain | hanya ke void, unknown, any | ke setiap tipe |
| Pemakaian umum | callback, event handler, fungsi dengan efek samping | fail(), assertNever(), perulangan tanpa akhir |
Perbedaan praktisnya terlihat saat narrowing. Setelah if (!user) fail("no user"), compiler tahu user sudah terdefinisi di baris berikutnya hanya kalau fail mengembalikan never. Dengan tipe kembalian void, compiler menganggap eksekusi bisa berlanjut.
Exhaustive Check dengan never
Setiap case dari switch atas sebuah union mempersempit tipe nilainya. Setelah setiap member ditangani, yang tersisa di default adalah never. Meng-assign-nya ke variabel never mengubah "aku sudah menangani semua kasus" menjadi sesuatu yang diverifikasi compiler:
Sekarang tambahkan member ketiga ke union tanpa menambahkan case-nya:
index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.
Error-nya menyebut member yang kamu lupakan. Tambahkan case "triangle": return (shape.base * shape.height) / 2; dan kodenya lolos kompilasi lagi. Helper assertNever adalah bentuk pemeriksaan yang sama yang bisa dipakai ulang, dan throw-nya tetap penting saat runtime: data dari JSON atau client versi lama bisa saja berisi kind yang menurut tipenya mustahil. Pola ini adalah tulang punggung discriminated union.
Narrowing sampai never
Hal yang sama terjadi pada narrowing apa pun, bukan hanya switch. Begitu semua kemungkinan sudah disingkirkan, variabelnya bertipe never:
Kalau nanti kamu melebarkan parameternya menjadi string | number | boolean | bigint, baris const nothing: never = x menjadi error, yang menunjuk ke fungsi yang perlu diperbarui.
never Menghilang di Union
never adalah himpunan nilai yang kosong, jadi menambahkannya ke union tidak mengubah apa pun: string | never sama saja dengan string. Begitulah conditional type menyaring union. Cabang yang mengembalikan never membuang member tersebut:
Exclude<T, U> dan Extract<T, U> bawaan bekerja persis dengan cara ini. Di intersection, kebalikannya yang berlaku: string & never adalah never.
Tipe yang Mustahil Menjadi never
Intersection yang tidak bisa dipenuhi nilai apa pun akan menyusut menjadi never:
type A = string & number; // never
type B = { kind: "a" } & { kind: "b" }; // never
Membaca properti dari nilai bertipe B akan melaporkan alasannya: Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. Kalau tipe yang kamu bangun ternyata menjadi never, cari dua bagian yang saling bertentangan.
never, unknown, dan any
| Tipe | Nilai yang ditampungnya | Bisa di-assign ke | Menerima |
|---|---|---|---|
unknown | setiap nilai | hanya unknown dan any | semuanya |
any | setiap nilai | semuanya kecuali never | semuanya |
never | tidak ada nilai | semuanya | hanya never |
unknown adalah puncak hierarki tipe, dan never adalah dasarnya. any sama sekali bukan bagian dari hierarki itu: ia mematikan pemeriksaan.
Pertanyaan yang sering diajukan
Apa itu tipe never di TypeScript?
never adalah tipe yang tidak punya nilai. Tidak ada yang bisa di-assign ke tipe ini (kecuali never lain), dan ia bisa di-assign ke setiap tipe. Tipe ini muncul sebagai tipe kembalian fungsi yang selalu melempar error atau berulang selamanya, sebagai tipe variabel setelah narrowing menyingkirkan semua kemungkinan, dan sebagai hasil dari tipe yang mustahil seperti string & number.
Apa perbedaan never dan void?
Fungsi yang mengembalikan void selesai secara normal; ia hanya tidak mengembalikan nilai yang berguna (saat runtime ia mengembalikan undefined). Fungsi yang mengembalikan never sama sekali tidak selesai: ia melempar error atau berjalan selamanya. Kode setelah pemanggilan fungsi never tidak bisa dijangkau, dan TypeScript memperlakukannya begitu saat narrowing.
Bagaimana cara melakukan exhaustive check di TypeScript?
Di cabang default dari switch atas sebuah union, assign nilainya ke variabel bertipe never, atau kirim ke fungsi assertNever(value: never): never yang melempar error. Kalau setiap kasus sudah ditangani, nilainya bertipe never di situ dan kodenya lolos kompilasi. Kalau ada kasus yang terlewat, compiler melaporkan bahwa member yang terlewat itu tidak bisa di-assign ke never.
Kenapa tipeku menjadi never?
Biasanya karena TypeScript sudah menyingkirkan semua pilihan lewat narrowing (misalnya setelah memeriksa typeof x === "string" dan typeof x === "number" pada string | number), atau karena sebuah intersection mustahil terpenuhi: string & number, atau dua tipe object yang properti bersamanya punya tipe literal yang bertentangan. Arahkan kursor ke tipenya di editor untuk melihat langkah mana yang menghasilkannya.
Apa arti "is not assignable to type never"?
Kode mencoba menaruh nilai sungguhan di tempat yang hanya mengizinkan never. Di exhaustive check, artinya ada member union yang belum ditangani. Di tempat lain, sering kali artinya sebuah array disimpulkan sebagai never[] atau sebuah intersection menyusut menjadi never; tambahkan anotasi atau perbaiki tipe yang bertentangan.