TypeScript. Tipe lanjutan

gambar



Halo para Penduduk! Kami telah mengirimkan lagi

" Professional TypeScript. Pengembangan aplikasi JavaScript yang dapat diskalakan " ke percetakan . Dalam buku ini, programmer yang sudah mahir dalam JavaScript akan belajar bagaimana menguasai TypeScript. Anda akan melihat bagaimana TypeScript dapat membantu Anda menskalakan kode Anda hingga 10x lebih baik dan membuat pemrograman menyenangkan lagi.



Di bawah ini adalah kutipan dari bab dari buku "Jenis Lanjutan".



Tipe lanjutan



Sistem tipe TypeScript yang terkenal di dunia bahkan mengejutkan para pemrogram Haskell dengan kemampuannya. Seperti yang sudah Anda ketahui, ini tidak hanya ekspresif, tetapi juga mudah digunakan: batasan jenis dan hubungan di dalamnya ringkas, dapat dimengerti, dan dalam banyak kasus disimpulkan secara otomatis.



Memodelkan elemen JavaScript dinamis seperti prototipe, mengikat ini, kelebihan beban, dan objek yang selalu berubah membutuhkan sistem tipe dan sistem tipe sekaya yang akan dilakukan Batman.



Saya akan memulai bab ini dengan mendalami topik subtipe, kompatibilitas, varian, variabel acak, dan ekstensi. Kemudian saya akan memperluas spesifikasi pemeriksaan tipe berbasis aliran, termasuk perbaikan dan totalitas. Selanjutnya, saya akan mendemonstrasikan beberapa fitur pemrograman tingkat lanjut di tingkat tipe: menghubungkan dan memetakan tipe objek, menggunakan tipe bersyarat, mendefinisikan perlindungan tipe, dan solusi fallback seperti pernyataan tipe dan pernyataan tugas eksplisit. Terakhir, saya akan memperkenalkan Anda pada beberapa pola lanjutan untuk meningkatkan keamanan tipe: pola objek pendamping, penyempurnaan antarmuka untuk tuple, meniru tipe nominal, dan ekstensi prototipe yang aman.



Hubungan antar tipe



Mari kita lihat lebih dekat hubungan di TypeScript.



Subtipe dan Supertipe



Kami telah menyentuh kompatibilitas di bagian Tentang Jenis di hal. 34, jadi mari selami topik ini, dimulai dengan definisi subtipe.

gambar




Kembali ke gbr. 3.1 dan lihat asosiasi subtipe bawaan TypeScript.

gambar




  • Array adalah subtipe dari suatu objek.
  • Tupel adalah subtipe dari larik.
  • Semuanya adalah subtipe dari apa pun.
  • tidak pernah merupakan subtipe dari segalanya.
  • Kelas Burung, yang merupakan kelanjutan dari kelas Hewan, adalah subtipe dari kelas Hewan.


Menurut definisi yang baru saja saya berikan untuk subtipe, ini berarti:



  • Dimanapun Anda membutuhkan sebuah objek, Anda dapat menggunakan sebuah array.
  • Dimanapun sebuah array dibutuhkan, tuple dapat digunakan.
  • Dimanapun Anda membutuhkannya, Anda dapat menggunakan suatu objek.
  • Tidak pernah bisa digunakan dimana-mana.
  • Dimanapun Anda membutuhkan Hewan, Anda dapat menggunakan Burung.


Supertipe adalah kebalikan dari subtipe.



SUPERTYPE



Jika Anda memiliki dua tipe, A dan B, dan B adalah supertipe dari A, maka Anda dapat dengan aman menggunakan A di mana pun B dibutuhkan (Gambar 6.2).


gambar


Dan lagi, berdasarkan diagram pada Gambar. 3.1:



  • Array adalah supertipe tuple.
  • Objek adalah supertipe larik.
  • Apa pun adalah supertipe dari segalanya.
  • Tidak pernah bukan supertipe siapa pun.
  • Hewan adalah supertipe Burung.


Ini hanyalah kebalikan dari subtipe dan tidak lebih.



Variasi



Untuk sebagian besar tipe, cukup mudah untuk memahami apakah tipe tertentu A adalah subtipe dari B. Untuk tipe sederhana seperti angka, string, dll., Anda dapat merujuk ke diagram pada Gambar. 3.1 atau secara independen menentukan bahwa nomor yang terdapat dalam nomor serikat | string adalah subtipe dari gabungan ini.



Tetapi ada jenis yang lebih kompleks, seperti obat generik. Pertimbangkan pertanyaan-pertanyaan ini:



  • Kapan Array <A> merupakan subtipe dari Array <B>?
  • Kapan Formulir A merupakan subtipe dari Formulir B?
  • Kapan fungsi (a: A) => B merupakan subtipe dari fungsi (c: C) => D?


Aturan subtipe untuk tipe yang berisi tipe lain (yaitu, memiliki parameter tipe seperti Array <A>, formulir dengan bidang seperti {a: number}, atau fungsi seperti (a: A) => B) lebih sulit untuk dipahami, karena tidak konsisten di berbagai bahasa pemrograman.



Untuk membuat aturan berikut lebih mudah dibaca, saya akan menyajikan beberapa elemen sintaks yang tidak berfungsi di TypeScript (jangan khawatir, ini bukan matematis):



  • A <: B berarti "A adalah subtipe dari yang sama dengan tipe B";
  • A>: B berarti "A adalah supertipe dari tipe B".


Variasi Bentuk dan Array



Untuk memahami mengapa bahasa tidak setuju dengan aturan untuk subtipe tipe kompleks, contoh dengan formulir yang menjelaskan pengguna dalam aplikasi akan membantu. Kami mewakilinya melalui beberapa jenis:



//  ,   .
type ExistingUser = {
    id: number
   name: string
}
//  ,     .
type NewUser = {
   name: string
}


Misalkan seorang magang di perusahaan Anda ditugaskan menulis kode untuk menghapus pengguna. Dia mulai dengan yang berikut:



function deleteUser(user: {id?: number, name: string}) {
    delete user.id
}
let existingUser: ExistingUser = {
    id: 123456,
    name: 'Ima User'
}
deleteUser(existingUser)


deleteUser menerima objek berjenis {id?: number, name: string} dan meneruskan existingUser bertipe {id: number, name: string} ke situ. Perhatikan bahwa tipe id properti (angka) adalah subtipe dari tipe yang diharapkan (angka | tidak ditentukan). Oleh karena itu, seluruh objek {id: number, name: string} adalah subtipe dari {id?: Number, name: string}, jadi TypeScript mengizinkannya.



Apakah Anda melihat masalah keamanan? Ada satu: setelah melewatkan ExistingUser ke deleteUser, TypeScript tidak tahu bahwa id pengguna telah dihapus, jadi jika Anda membaca existingUser.id setelah menghapusnya deleteUser (existingUser), maka TypeScript akan tetap menganggap bahwa existingUser.id adalah jenis nomor.



Jelas, menggunakan tipe objek yang diharapkan supertipe-nya tidak aman. Jadi mengapa TypeScript mengizinkan ini? Intinya adalah bahwa itu tidak dimaksudkan untuk sepenuhnya aman. Sistem tipenya berusaha menangkap kesalahan nyata dan membuatnya terlihat oleh pemrogram dari semua tingkatan. Karena pembaruan destruktif (seperti menghapus properti) relatif jarang dalam praktiknya, TypeScript bersifat santai dan memungkinkan Anda menetapkan objek di tempat yang diharapkan supertipe-nya.



Dan bagaimana dengan kasus sebaliknya: apakah mungkin untuk menetapkan objek di mana subtipe yang diharapkan?



Mari tambahkan tipe baru untuk pengguna lama, lalu hapus pengguna dengan tipe itu (bayangkan menambahkan tipe ke kode yang ditulis rekan Anda):



type LegacyUser = {
    id?: number | string
    name: string
}
let legacyUser: LegacyUser = {
    id: '793331',
    name: 'Xin Yang'
}
deleteUser(legacyUser) //  TS2345: a  'LegacyUser'
                                  //    
                                  // '{id?: number |undefined, name: string}'.
                                 //  'string'    'number |
                                 // undefined'.


Saat Anda mengirimkan formulir dengan properti yang tipenya adalah supertipe dari tipe yang diharapkan, TypeScript bersumpah. Ini karena id adalah string | nomor | undefined dan deleteUser hanya menangani kasus di mana id adalah angka | tidak terdefinisi.



Saat mengharapkan formulir, Anda bisa meneruskan tipe dengan tipe properti yang <: dari tipe yang diharapkan, tapi Anda tidak bisa meneruskan formulir tanpa tipe properti yang supertypes dari tipe yang diharapkan. Saat kita berbicara tentang tipe, kita berkata, "Bentuk TypeScript (objek dan kelas) adalah kovarian dalam tipe propertinya." Artinya, agar objek A dapat ditetapkan ke objek B, masing-masing propertinya harus <: properti yang sesuai di B.



Kovarian adalah salah satu dari empat jenis varian:



Invarians Dibutuhkan secara

khusus T.

Kovarian

Dibutuhkan <: T.

Kontravarian

Dibutuhkan>: T.

Bivarian Akan

cocok dengan <: T atau>: T.



Dalam TypeScript, setiap tipe kompleks adalah kovarian dalam anggotanya - objek, kelas, larik, dan tipe kembalian fungsi - dengan satu pengecualian: tipe parameter fungsi bersifat kontravarian.



. , . ( ). , Scala, Kotlin Flow, , .



TypeScript : , , , (, id deleteUser, , , ).


Variasi fungsi



Mari pertimbangkan beberapa contoh.



Fungsi A adalah subtipe dari fungsi B jika A memiliki arity (jumlah parameter) yang sama atau lebih kecil dari B, dan:



  1. Tipe this, milik A, tidak terdefinisi, atau>: dari tipe this, milik B.
  2. Masing-masing parameter A>: parameter yang sesuai di B.
  3. Tipe pengembalian A <: tipe pengembalian B.


Perhatikan bahwa agar fungsi A menjadi subtipe dari fungsi B, jenis dan parameternya harus>: padanannya di B, sedangkan jenis kembaliannya harus <:. Mengapa kondisinya berbalik? Mengapa kondisi <: sederhana tidak berfungsi untuk setiap komponen (tipe ini, tipe parameter, dan tipe kembalian), seperti halnya dengan objek, array, unions, dan sebagainya?



Mari kita mulai dengan mendefinisikan tiga tipe (alih-alih kelas, Anda dapat menggunakan tipe lain, di mana A: <B <: C):



class Animal {}
class Bird extends Animal {
    chirp() {}
}
class Crow extends Bird {
    caw() {}
}


Jadi Gagak <: Burung <: Hewan.



Mari tentukan fungsi yang mengambil Bird dan membuatnya berkicau:



function chirp(bird: Bird): Bird {
    bird.chirp()
    return bird
}


Sejauh ini baik. Apa yang TypeScript memungkinkan Anda untuk menyalurkan ke chirp?



chirp(new Animal) //  TS2345:   'Animal'
chirp(new Bird) //     'Bird'.
chirp(new Crow)


Instance Bird (sebagai parameter kicauan dari tipe burung) atau instans Gagak (sebagai subtipe Bird). Pengoperan subtipe bekerja seperti yang diharapkan.



Mari buat fungsi baru. Kali ini parameternya akan menjadi fungsi:



function clone(f: (b: Bird) => Bird): void {
    // ...
}


klon membutuhkan fungsi f yang mengambil Bird dan mengembalikan Bird. Jenis fungsi apa yang dapat diteruskan ke f dengan aman? Jelas sekali, fungsi yang menerima dan mengembalikan Bird:



function birdToBird(b: Bird): Bird {
    // ...
}
clone(birdToBird) // OK


Bagaimana dengan fungsi yang mengambil seekor Burung tetapi mengembalikan seekor Gagak atau Hewan?



function birdToCrow(d: Bird): Crow {
    // ...
}
clone(birdToCrow) // OK
function birdToAnimal(d: Bird): Animal {
    // ...
}
clone(birdToAnimal) //  TS2345:   '(d: Bird) =>
                             // Animal'    
                            // '(b: Bird) => Bird'. 'Animal'
                           //    'Bird'.


birdToCrow bekerja seperti yang diharapkan, tetapi birdToAnimal membuat kesalahan. Mengapa? Bayangkan implementasi klon terlihat seperti ini:



function clone(f: (b: Bird) => Bird): void {
    let parent = new Bird
    let babyBird = f(parent)
    babyBird.chirp()
}


Dengan meneruskan fungsi f untuk mengkloning, yang mengembalikan Animal, kita tidak bisa memanggil .chirp di dalamnya. Oleh karena itu, TypeScript harus memastikan bahwa fungsi yang kami berikan mengembalikan setidaknya Bird.



Ketika kita mengatakan bahwa fungsi adalah kovarian dalam tipe kembaliannya, itu berarti bahwa suatu fungsi bisa menjadi subtipe dari fungsi lain hanya jika tipe kembaliannya adalah <: tipe kembalian dari fungsi itu.



Oke, lalu bagaimana dengan tipe parameter?



function animalToBird(a: Animal): Bird {
  // ...
}
clone(animalToBird) // OK
function crowToBird(c: Crow): Bird {
  // ...
}
clone(crowToBird)        //  TS2345:   '(c: Crow) =>
                        // Bird'     '
                       // (b: Bird) => Bird'.


Agar suatu fungsi kompatibel dengan fungsi lain, semua tipe parameternya (termasuk ini) harus>: parameternya yang sesuai di fungsi lain. Untuk memahami alasannya, pikirkan tentang bagaimana pengguna dapat mengimplementasikan crowToBird sebelum meneruskannya ke klon?



function crowToBird(c: Crow): Bird {
  c.caw()
  return new Bird
}


TSC-: STRICTFUNCTIONTYPES



- TypeScript this. , , {«strictFunctionTypes»: true} tsconfig.json.



{«strict»: true}, .


Sekarang, jika klon memanggil crowToBird dengan Burung baru, kita akan mendapatkan pengecualian, karena .caw didefinisikan di semua Burung Gagak tetapi tidak semua Burung.



Ini berarti bahwa fungsi bertentangan dalam parameternya dan tipe ini. Artinya, suatu fungsi dapat menjadi subtipe dari fungsi lain hanya jika masing-masing parameter dan jenisnya adalah>: parameternya yang sesuai di fungsi lain.



Untungnya, aturan ini tidak perlu dihafal. Ingatlah mereka ketika editor memberi garis bawah merah ketika Anda melewatkan fungsi yang salah ketik di suatu tempat.



Kesesuaian



Hubungan subtipe dan supertipe adalah konsep kunci dalam bahasa apa pun yang diketik secara statis. Mereka juga penting untuk memahami cara kerja kompatibilitas (ingat, kompatibilitas mengacu pada aturan TypeScript yang mengatur penggunaan tipe A di mana tipe B diperlukan).



Ketika TypeScript perlu menjawab pertanyaan, "Apakah tipe A kompatibel dengan tipe B?", Ini mengikuti aturan sederhana. Untuk tipe non-enum - seperti array, boolean, angka, objek, fungsi, kelas, instance kelas, dan string, termasuk tipe literal - A kompatibel dengan B jika salah satu kondisinya benar.



  1. A <: B.
  2. A adalah salah satu.


Aturan 1 hanyalah definisi subtipe: jika A adalah subtipe B, maka di mana pun B diperlukan, Anda dapat menggunakan A.



Aturan 2 adalah pengecualian untuk Aturan 1 untuk kemudahan interaksi dengan kode JavaScript.

Untuk tipe pencacahan yang dibuat dengan kata kunci enum atau const enum, tipe A kompatibel dengan pencacahan B jika salah satu syaratnya benar.



  1. A adalah anggota pencacahan B.
  2. B memiliki setidaknya satu anggota dengan tipe nomor, dan A adalah nomor.


Aturan 1 persis sama dengan tipe sederhana (jika A adalah anggota B, maka A adalah tipe B dan kita katakan B <: B).



Aturan 2 diperlukan untuk kenyamanan bekerja dengan enumerasi, yang secara serius membahayakan keamanan TypeScript (lihat sub-bagian "Enum" di halaman 60), dan saya merekomendasikan untuk menghindarinya.



Ekspansi tipe Ekspansi tipe



adalah kunci untuk memahami cara kerja inferensi tipe. TypeScript fleksibel dalam eksekusi dan lebih cenderung keliru dalam menyimpulkan tipe yang lebih umum daripada menyimpulkan sespesifik mungkin. Ini akan membuat hidup Anda lebih mudah dan mengurangi waktu yang dibutuhkan untuk menangani catatan pemeriksa tipe.



Anda telah melihat beberapa contoh ekspansi tipe di Bab 3. Pertimbangkan orang lain.



Saat Anda mendeklarasikan variabel sebagai bisa berubah (dengan let atau var), tipenya akan meluas dari tipe nilai literal ke tipe basis tempat literal berada:



let a = 'x' // string
let b = 3   // number
var c = true   // boolean
const d = {x: 3}   // {x: number}
enum E {X, Y, Z}
let e = E.X   // E


Ini tidak berlaku untuk deklarasi yang tidak dapat diubah:



const a = 'x' // 'x'
const b = 3   // 3
const c = true   // true
enum E {X, Y, Z}
const e = E.X   // E.X


Anda dapat menggunakan anotasi tipe eksplisit untuk mencegahnya meluas:



let a: 'x' = 'x' // 'x'
let b: 3 = 3  // 3
var c: true = true  // true
const d: {x: 3} = {x: 3}  // {x: 3}


Saat Anda menetapkan kembali tipe yang tidak diperpanjang dengan let atau var, TypeScript akan memperluasnya untuk Anda. Untuk mencegahnya, tambahkan anotasi tipe eksplisit ke deklarasi asli:



const a = 'x' // 'x'
let b = a  // string
const c: 'x' = 'x'  // 'x'
let d = c  // 'x'


Variabel yang diinisialisasi menjadi null atau tidak ditentukan diperluas menjadi:



let a = null // any
a = 3  // any
a = 'b'  // any


Namun, ketika sebuah variabel, yang diinisialisasi ke null atau undefined, meninggalkan ruang lingkup di mana ia dideklarasikan, TypeScript menugaskannya ke tipe tertentu:



function x() {
   let a = null  // any
   a = 3   // any
   a = 'b'   // any
   return a
}
x()   // string






Tipe const Tipe const membantu menghindari perluasan deklarasi tipe. Gunakan sebagai pernyataan tipe (lihat subbagian "Persetujuan jenis" di halaman 185):



let a = {x: 3}   // {x: number}
let b: {x: 3}    // {x: 3}
let c = {x: 3} as const   // {readonly x: 3}


const menghilangkan perluasan tipe dan secara rekursif menandai anggotanya sebagai hanya baca, bahkan dalam struktur data yang sangat bertingkat:



let d = [1, {x: 2}]              // (number | {x: number})[]
let e = [1, {x: 2}] as const    // readonly [1, {readonly x: 2}]


Gunakan sebagai const saat Anda ingin TypeScript menyimpulkan sesempit mungkin.



Memeriksa Untuk



Ekspansi Jenis Properti Ekstra juga ikut bermain saat TypeScript memeriksa apakah satu jenis objek kompatibel dengan jenis objek lain.



Tipe objek adalah kovarian dalam anggotanya (lihat subbagian "Variasi Bentuk dan Array" di halaman 148). Tapi, jika TypeScript mengikuti aturan ini tanpa pemeriksaan tambahan, masalah bisa muncul.



Misalnya, pertimbangkan objek Opsi yang bisa Anda berikan ke kelas untuk menyesuaikannya:



type Options = {
    baseURL: string
    cacheSize?: number
    tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({
     baseURL: 'https://api.mysite.com',
     tier: 'prod'
})


Apa yang terjadi sekarang jika Anda membuat kesalahan dalam opsi?



new API({
   baseURL: 'https://api.mysite.com',
   tierr: 'prod'         //  TS2345:   '{tierr: string}'
})                      //     'Options'.
                        //     
                       //  ,  'tierr'  
                      //   'Options'.    'tier'?


Ini adalah bug JavaScript yang umum, dan ada baiknya TypeScript membantu Anda menangkapnya. Tetapi jika tipe objek adalah kovarian dalam anggotanya, bagaimana TypeScript mencegatnya?



Dengan kata lain:



  • Kami mengharapkan jenis {baseURL: string, cacheSize?: Number, tier?: 'Prod' | 'dev'}.
  • Kami meneruskan jenis {baseURL: string, tierr: string}.
  • Jenis yang diteruskan adalah subtipe dari jenis yang diharapkan, tetapi TypeScript tahu cara melaporkan kesalahan.


Dengan memeriksa properti tambahan , saat Anda mencoba menetapkan tipe literal objek baru T ke tipe lain, U, dan T memiliki properti yang tidak dimiliki U, TypeScript melaporkan kesalahan. Tipe literal objek



baru adalah tipe yang disimpulkan TypeScript dari literal objek. Jika literal objek ini menggunakan pernyataan tipe (lihat sub-bagian “Pernyataan Tipe” di halaman 185) atau ditetapkan ke variabel, maka tipe baru akan diperluas ke tipe objek biasa dan kebaruannya hilang. Mari kita coba membuat definisi ini lebih luas:







type Options = {
     baseURL: string
     cacheSize?: number
     tier?: 'prod' | 'dev'
}
class API {
    constructor(private options: Options) {}
}
new API({ ❶
    baseURL: 'https://api.mysite.com',
    tier: 'prod'
})
new API({ ❷
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2345:   '{baseURL:
}) // string; badTier: string}' 
//    'Options'.
new API({ ❸
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
} as Options)
let badOptions = { ❹
    baseURL: 'https://api.mysite.com',
    badTier: 'prod'
}
new API(badOptions)
let options: Options = { ❺
    baseURL: 'https://api.mysite.com',
    badTier: 'prod' //  TS2322:  '{baseURL: string;
} // badTier: string}'  
// 'Options'.
new API(options)


❶ Buat instance API dengan baseURL dan salah satu dari dua properti opsional: tier. Semuanya bekerja.



❷ Kami keliru menulis tingkat sebagai badTier. Objek opsi yang kami berikan ke API baru adalah baru (tipenya disimpulkan, tidak kompatibel dengan variabel, dan kami tidak mengetikkan pernyataan untuknya), jadi saat memeriksa properti yang tidak perlu, TypeScript mendeteksi properti badTier ekstra (yang didefinisikan dalam objek opsi, tetapi tidak dalam tipe Options).



❸ Buat pernyataan bahwa objek opsi yang tidak valid adalah tipe Opsi. TypeScript tidak lagi melihatnya sebagai baru dan menyimpulkan dari pemeriksaan properti tambahan bahwa tidak ada kesalahan. Sintaks as T dijelaskan di bagian "Jenis Pernyataan" di hal. 185.



❹ Menetapkan objek opsi ke variabel badOptions. TypeScript tidak lagi menganggapnya sebagai baru dan, setelah memeriksa properti yang tidak perlu, menyimpulkan bahwa tidak ada kesalahan.



❺ Saat kami secara eksplisit mengetik opsi sebagai Opsi, objek yang kami tetapkan ke opsi adalah baru, jadi TypeScript memeriksa properti tambahan dan menemukan bug. Perhatikan bahwa dalam hal ini pemeriksaan properti tambahan tidak dilakukan saat kita meneruskan opsi ke API baru, tetapi melakukannya saat kita mencoba menetapkan objek opsi ke variabel opsi.



Anda tidak perlu menghafal aturan ini. Ini hanyalah heuristik TypeScript internal untuk menangkap bug sebanyak mungkin. Ingat saja jika Anda tiba-tiba bertanya-tanya bagaimana TypeScript menemukan bug yang bahkan Ivan - pengatur waktu lama perusahaan Anda dan juga sensor kode profesional - tidak menyadarinya.



Perbaikan



TypeScript melakukan eksekusi simbolis dari inferensi tipe. Modul pemeriksaan tipe menggunakan instruksi aliran perintah (seperti if,?, ||, dan switch) bersama dengan kueri tipe (seperti tipe, instanceof, dan dalam), dengan demikian mengkualifikasikan tipe saat programmer membaca kode. Namun, fitur praktis ini didukung dalam beberapa bahasa.



Bayangkan Anda telah mengembangkan API untuk menentukan aturan CSS di TypeScript, dan kolega Anda ingin menggunakannya untuk menyetel lebar elemen HTML. Ini menyampaikan lebar yang ingin Anda parse dan periksa nanti.



Pertama, mari terapkan fungsi untuk mengurai string CSS menjadi nilai dan unit:



//       
//  ,      CSS
type Unit = 'cm' | 'px' | '%'
//   
let units: Unit[] = ['cm', 'px', '%']
//   . .   null,    
function parseUnit(value: string): Unit | null {
  for (let i = 0; i < units.length; i++) {
    if (value.endsWith(units[i])) {
       return units[i]
}
}
     return null
}


Kami kemudian menggunakan parseUnit untuk mengurai lebar yang disediakan pengguna. width bisa berupa angka (mungkin dalam piksel), atau string dengan unit terpasang, atau null, atau tidak ditentukan.



Dalam contoh ini, kami menggunakan kualifikasi tipe beberapa kali:



type Width = {
     unit: Unit,
     value: number
}
function parseWidth(width: number | string | null |
undefined): Width | null {
//  width — null  undefined,  .
if (width == null) { ❶
     return null
}
//  width — number,  .
if (typeof width === 'number') { ❷
    return {unit: 'px', value: width}
}
//      width.
let unit = parseUnit(width)
if (unit) { ❸
return {unit, value: parseFloat(width)}
}
//     null.
return null
}


❶ TypeScript dapat memahami bahwa persamaan longgar JavaScript terhadap null akan mengembalikan nilai true untuk null dan tidak ditentukan. Ia juga mengetahui bahwa jika check tersebut lolos, maka kami akan melakukan pengembalian, dan jika kami tidak melakukan pengembalian, maka pemeriksaan tersebut gagal dan sejak saat itu, tipe lebar adalah angka | string (tidak bisa lagi null atau undefined). Kami mengatakan bahwa jenis itu disempurnakan dari angka | string | null | undefined in number | tali.



❷ Pemeriksaan typeof meminta nilai pada waktu proses untuk melihat tipenya. TypeScript juga memanfaatkan typeof pada waktu kompilasi: di cabang if tempat pengujian berhasil, TypeScript mengetahui bahwa lebar adalah angka. Jika tidak (jika cabang ini mengembalikan) lebar harus berupa string - satu-satunya tipe yang tersisa.



❸ Karena parseUnit dapat mengembalikan null, kami memeriksa ini. TypeScript mengetahui bahwa jika unit benar maka itu harus dari tipe Unit di cabang if. Jika tidak, unit tidak benar, yang berarti tipenya null (disaring dari Unit | null).



❹ Akhirnya, kami mengembalikan nol. Ini hanya dapat terjadi jika pengguna memasukkan string untuk lebar, tetapi string itu berisi unit yang tidak didukung.

Saya mempelajari alur pemikiran TypeScript untuk setiap penyempurnaan jenis yang dibuat. TypeScript melakukan pekerjaan yang bagus untuk mengambil alasan Anda saat Anda membaca dan menulis kode dan mengkristalkannya ke dalam pemeriksaan jenis dan urutan inferensi.



Jenis gabungan terdiskriminasi



Seperti yang baru kita ketahui, TypeScript memiliki pemahaman yang baik tentang cara kerja JavaScript dan mampu melacak kualifikasi jenis kita seolah-olah membaca pikiran kita.



Katakanlah kita sedang membuat sistem acara khusus untuk sebuah aplikasi. Kita mulai dengan mendefinisikan tipe event bersama dengan fungsi yang menangani kedatangan event tersebut. Bayangkan UserTextEvent mensimulasikan peristiwa keyboard (misalnya, pengguna mengetik teks <input />), dan UserMouseEvent mensimulasikan peristiwa mouse (pengguna menggerakkan mouse pada koordinat [100, 200]):



type UserTextEvent = {value: string}
type UserMouseEvent = {value: [number, number]}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
     if (typeof event.value === 'string') {
         event.value // string
         // ...
         return
   }
         event.value // [number, number]
}


TypeScript tahu bahwa di dalam blok if, event.value harus berupa string (berkat pemeriksaan typeof), yaitu, event.value setelah blok if harus berupa tupel [angka, angka] (karena kembalian di blok if).



Komplikasi akan mengarah pada apa? Mari tambahkan klarifikasi ke jenis acara:



type UserTextEvent = {value: string, target: HTMLInputElement}
type UserMouseEvent = {value: [number, number], target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
    if (typeof event.value === 'string') {
        event.value // string
        event.target // HTMLInputElement | HTMLElement (!!!)
        // ...
        return
   }
  event.value // [number, number]
  event.target // HTMLInputElement | HTMLElement (!!!)
}


Meskipun penyempitan berfungsi untuk event.value, penyempitan tidak berfungsi untuk event.target. Mengapa? Ketika pegangan menerima parameter tipe UserEvent, itu tidak berarti bahwa Anda harus meneruskan UserTextEvent atau UserMouseEvent ke sana - pada kenyataannya, Anda bisa memberikan argumen tipe UserMouseEvent | UserTextEvent. Dan karena anggota serikat dapat tumpang tindih, TypeScript membutuhkan cara yang lebih andal untuk mengetahui kapan dan kasus serikat mana yang relevan.



Anda dapat melakukan ini dengan menggunakan tipe literal dan definisi tag untuk setiap kasus tipe gabungan. Tag yang bagus:



  • Dalam setiap kasus, itu terletak di tempat yang sama dengan tipe serikat. Menyiratkan bidang objek yang sama saat menggabungkan tipe objek, atau indeks yang sama saat menggabungkan tupel. Dalam praktiknya, serikat pekerja yang terdiskriminasi lebih sering menjadi objek.
  • Diketik sebagai tipe literal (string literal, numerik, boolean, dll.). Anda dapat mencampur dan mencocokkan berbagai jenis literal, tetapi yang terbaik adalah tetap menggunakan satu jenis. Biasanya ini adalah jenis string literal.
  • Tidak universal. Tag tidak boleh menerima argumen tipe umum.
  • Saling eksklusif (unik dalam tipe persatuan).


Mari perbarui jenis acara dengan mempertimbangkan hal di atas:



type UserTextEvent = {type: 'TextEvent', value: string,
                                        target: HTMLInputElement}
type UserMouseEvent = {type: 'MouseEvent', value: [number, number],
                                        target: HTMLElement}
type UserEvent = UserTextEvent | UserMouseEvent
function handle(event: UserEvent) {
   if (event.type === 'TextEvent') {
       event.value // string
       event.target // HTMLInputElement
       // ...
       return
   }
  event.value // [number, number]
  event.target // HTMLElement
}


Sekarang, ketika kita memperbaiki acara berdasarkan nilai bidang yang diberi tag (event.type), TypeScript tahu bahwa seharusnya ada UserTextEvent di cabang if, dan setelah cabang if, itu harus menjadi UserMouseEvent. Karena tag unik di setiap jenis gabungan, TypeScript tahu bahwa mereka saling eksklusif.



Gunakan gabungan terdiskriminasi saat menulis fungsi yang menangani berbagai kasus tipe gabungan. Misalnya, saat bekerja dengan tindakan Flux, pemulihan redux, atau gunakan Reducer di React.



Anda dapat membiasakan diri dengan buku lebih detail dan memesan di muka dengan harga khusus di situs web penerbit



All Articles