typescript - Bekerja dengan Tipe Generik dan Tipe Union
Dengan kode ini, saya pikir sudah jelas apa yang ingin saya capai, ada ide untuk mencoba mendapatkan kode sesuai yang diharapkan?
interface Person {
name: "Peter" | "Robert";
age: number;
isPerson: true
}
interface Animal {
name: "Jerry" | "Tom";
age: number;
isPerson: false
}
type LivingBeing = Person | Animal
function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"] {
const names: T["name"][] = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random()*names.length)];
}
const person: "Peter" | "Robert" = getRandomName(true) // Only valid values are "Peter" | "Robert". Not "Peter" | "Robert" | "Jerry" | "Tom" as the compiler says. Check it in the Playground TS
Jawaban #1
T
Masalah yang Anda alami adalah bahwa pada tanda panggilan berikut,
declare function getRandomName<T extends LivingBeing>(isPerson: T["isPerson"]): T["name"];
tidak ada situs inferensi yang baik untuk parameter tipe generik T. Anda berharap kompilator akan melihat isPerson dengan tipe T["isPerson"], dan menggunakannya untuk menambahkan inferensi T. Sayangnya, itu tidak terjadi; tipe akses terindeks dengan bentuk T[K] tidak dapat digunakan untuk menambahkan inferensi baik T maupun K.
Pada satu titik ada sebuah pull request dari salah satu anggota tim TS, microsoft/TypeScript#20126, yang seharusnya menambahkan dukungan untuk ini. Sayangnya, itu tidak pernah digabungkan ke cabang utama dan tidak menjadi bagian dari bahasa. Tidak jelas alasannya dari riwayat isu. Ya sudah.
Tanpa situs inferensi untuk parameter tipe generik T, dan jika Anda tidak menentukan secara manual seperti
getRandomName<Person>(true);
maka inferensi akan gagal dan kompiler kembali ke batasannya:
getRandomName(true);
// function getRandomName<LivingBeing>(
// isPerson: boolean): "Peter" | "Robert" | "Jerry" | "Tom"
Karena T dibatasi menjadi LivingBeing, maka disimpulkan sebagai LivingBeing, dan LivingBeing["isPerson"] hanyalah boolean, serta LivingBeing["name"] adalah gabungan dari empat nama. Dan Anda telah kehilangan setiap kaitan antara true dan false dengan tipe keluaran. Ups.
SOLUSI: SEDIAKAN SITUS INFERENSI UNTUK T
Jika Anda ingin memanggil getRandomName() untuk menginferensi parameter tipe dengan benar, Anda perlu memberi compiler sebuah situs inferensi yang mudah. Cara paling gampang dan langsung untuk melakukan ini adalah dengan membuat parameter tipe persis sama dengan tipe parameter fungsi yang dilewatkan. Anda sedang melewatkan isPerson, jadi mari kita buat tipe isPerson menjadi parameter tipe yang akan kita sebut P. Kita harus membatasi P ke tipe yang valid untuk isPerson, yaitu LivingBeing["isPerson"] (juga disebut boolean, tapi kita biarkan seperti ini). Jadi tanda tangan pemanggilannya akan terlihat seperti ini:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): ???;
Sebagai pengganti ??? kita perlu menghitung tipe output yang diinginkan. Pertama, diberikan P, anggota dari union Person mana yang sesuai? Ini pada dasarnya membedakan union yang dibedakan pada tingkat tipe. Kita bisa menggunakan tipe kondisional untuk itu: secara khusus, tipe utilitas Extract<T, U>:
// this is just to demonstrate; we don't need to use the type alias
type DiscriminateLivingBeing<P extends LivingBeing["isPerson"]> =
Extract<LivingBeing, { isPerson: P }>;
type TrueType = DiscriminateLivingBeing<true> // Person
type FalseType = DiscriminateLivingBeing<false> // Animal
Dan setelah kita memilikinya, kita hanya perlu melihat properti name. Jadi berikut adalah tanda panggilan baru:
declare function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
Dan kita bisa mengujinya:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
Terlihat bagus. Kita selesai, kan? Eh, belum sepenuhnya:
KERIPUTAN: TANDA PANGGILAN KONDISIONAL GENERIK SULIT DIIMPLEMENTASIKAN
Namun, jika Anda mencoba menerapkan getRandomName() persis seperti sebelumnya, Anda akan menghadapi masalah:
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"] {
const names: Extract<LivingBeing, { isPerson: P }>["name"][] = // error!
isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)]; // error!
}
Di dalam implementasi fungsi, P adalah parameter tipe generik yang belum terselesai atau tidak ditentukan. Dan tipe Extract<LivingBeing, {isPerson: P}>["name"][] adalah tipe kondisional yang bergantung padanya. Dan ketika tipe kondisional bergantung pada parameter tipe yang belum terselesai, compiler pada dasarnya menyerah untuk mencoba memahami apakah nilai tertentu dapat ditetapkan kepadanya. Ia menunda mengevaluasinya sepenuhnya. Jadi meskipun seorang manusia bisa menelusuri berbagai kemungkinan untuk P dan meyakinkan dirinya bahwa isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"] dapat ditetapkan ke Extract<LivingBeing, {isPerson: P}>["name"][], compiler tidak mampu melakukan ini. Jadi compiler mengeluh.
Ada permintaan fitur di microsoft/TypeScript#33912 yang meminta dukungan yang lebih baik untuk mengimplementasikan fungsi-fungsi yang tanda panggilannya melibatkan tipe kondisional generik. Untuk saat ini, Anda harus mengatasinya dengan sesuatu seperti pernyataan tipe untuk membungkam peringatan compiler.
Biasanya dalam kasus seperti ini saya membuat fungsi menjadi overload dengan satu tanda panggilan saja. Implementasi pernyataan fungsi overload diperiksa lebih longgar, jadi saya membuat tanda panggilan tersebut sepenuhnya benar:
// call signature
function getRandomName<P extends LivingBeing["isPerson"]>(
isPerson: P
): Extract<LivingBeing, { isPerson: P }>["name"];
Dan kemudian tipe input dan output implementasi fungsi diperluas cukup untuk membuat implementasi tersebut bekerja:
// implementation
function getRandomName(isPerson: LivingBeing["isPerson"]): LivingBeing["name"] {
const names = isPerson ? ["Peter", "Robert"] as const : ["Jerry", "Tom"] as const
return names[Math.floor(Math.random() * names.length)];
}
Saya menggunakan penegasan const sehingga kompiler akan melacak tipe literal string dari string dalam array. Tapi selain itu, ini adalah implementasi yang sederhana. Jika Anda benar-benar ingin, Anda bisa lebih melonggarkannya lagi dan bahkan meniadakannya:
// implementation
function getRandomName(isPerson: boolean): string {
const names = isPerson ? ["Peter", "Robert"] : ["Jerry", "Tom"]
return names[Math.floor(Math.random() * names.length)];
}
Bagaimanapun, sekarang tidak ada kesalahan, dan Anda dapat memanggil fungsi sesuai keinginan Anda:
const person = getRandomName(true)
// const person: "Peter" | "Robert"
const animal = getRandomName(false)
// const animal: "Jerry" | "Tom"
Tautan playground ke kode
