Tipe Typescript {} dianggap identik dengan tipe non-nullish lainnya
Saya memiliki kelas prosesor dengan fungsi concat yang mengambil instance prosesor lain yang akan diproses dan kemudian mengembalikan sekali lagi instance prosesor lain.
Saya ingin agar fungsi concat mengambil parameter concatenator yang memiliki tipe yang sama dengan generik V. Ini bekerja dengan baik dalam sebagian besar kasus, tetapi tidak jika saya menggunakan tipe {}. Ini seharusnya tidak berfungsi tetapi ternyata berfungsi. Saya juga mencoba menggunakan NoInfer tetapi itu tidak berhasil.
PS: Saya tahu bahwa tipe {} mewakili tipe non-nullish apa pun, jadi saya membutuhkannya untuk menimbulkan kesalahan kompilasi karena secara teori bisa mengambil tipe non-nullish apa pun, yang saya tidak inginkan karena saya ingin itu memiliki tipe V yang sama.
Bagaimana saya bisa memperbaikinya??
Kode :
class Processor<T, V> {
current: T
next : V
constructor(current: T, next: V) {
this.current = current
this.next = next
}
concat<U>(concatenator: Processor<U, V>): Processor<U, V> {
// Do something
// Fake return
return '' as unknown as Processor<U, V>
}
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a)
Jawaban #1
Tipe Processor<T, V> Anda bersifat kovarian pada V (lihat Perbedaan antara Variansi, Kovarians, Kontravarians, Bivarians, dan Invarian dalam TypeScript) jadi jika V1 extends V2 maka Processor<T, V1> extends Processor<T, V2>. Dan karena tipe kosong {} memang merepresentasikan setiap nilai yang bukan null, dan karena string extends {}, maka Processor<string, string> extends Processor<string, {}>. Ini semua adalah TypeScript yang bekerja sesuai desain, dan jika ini tidak menyebabkan kesalahan saat runtime, maka bisa dikatakan sebaiknya Anda biarkan seperti ini.
Sistem tipe TypeScript tidak memiliki "tipe tepat" seperti yang diminta di microsoft/TypeScript#12936, jadi Anda tidak bisa dengan mudah mengatakan "properti ini dan tidak lebih", sehingga tidak ada Exact<{}> sederhana yang bisa Anda gunakan sebagai pengganti {} untuk tujuan ini. Nah, ada sesuatu yang agak mendekati; Anda bisa mencoba {[k: string]: never} sebagai pengganti {}:
let c: Processor<string, { [k: string]: never }> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
// Type 'string' is not assignable to type '{ [k: string]: never; }'.
Tetapi {[k: string]: never} itu rapuh, dan tidak bisa diperluas ke situasi dengan jumlah properti yang terbatas dan tidak nol.
Jika kasus penggunaan Anda benar-benar mengharuskan bahwa Processor<T, V1> extends Processor<T, V2> hanya jika V1 dan V2 adalah tipe yang sama (atau dapat saling ditugaskan), maka yang Anda inginkan adalah agar Processor<T, V> bersifat invarian dalam V. Ada cara agar itu terjadi secara alami, jika beberapa anggota dari kelas bersifat kontravarian dalam V, maka seluruhnya menjadi invarian:
class Processor<T, V> {
next: V // next is covariant in V
randomThing?: (x: V) => void; // randomThing is contravariant in V
} // whole thing is invariant in V
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
Atau, jika Anda ingin secara manual memastikan variansi yang lebih ketat, Anda dapat menggunakan anotasi variansi pada parameter tipe. Modifier out berarti "tidak kontravarian" dan modifier in berarti "tidak kovarian", sehingga invarian ditentukan oleh in out ("tidak kontravarian maupun kovarian"):
class Processor<T, in out V> {
// ⋯
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
Saran saya adalah memastikan bahwa Processor<T, V> secara alami invariant dalam V dengan menambahkan anggota yang menjamin hal ini, daripada menggunakan penanda varian atau permainan tipe yang lebih rumit. Jika Anda tidak bisa melakukan itu karena Anda benar-benar tidak membutuhkan anggota tambahan, maka saya akan kembali ke saran awal saya: biarkan saja seperti itu jika Anda tidak menghadapi kesalahan runtime yang sebenarnya karena hal ini. Sistem tipe TypeScript tidak sempurna, tetapi kovarians berguna dan jika TypeScript menganggap Processor<T, V> adalah kovarian dalam V maka Anda mungkin akan benar-benar mendapatkan manfaat darinya. Apakah Anda benar-benar ingin TS mengeluh jika ia menafsirkan Processor<T, "a"> padahal Anda ingin menggunakan Processor<T, string>? Jika V bersifat invarian, maka kedua tipe tersebut tidak terkait. Jika Anda berpikir Processor<T, "a"> extends Processor<T, string> adalah hal yang baik, maka biarkan kode Anda apa adanya dan tangani {} dengan cara lain.
Tautan playground ke kode
