TypeScript Lanjutan

Freediving - scuba diving tanpa scuba diving. Penyelam merasakan hukum Archimedes: ia memindahkan sejumlah air, yang mendorongnya kembali. Oleh karena itu, beberapa meter pertama diberikan yang paling sulit, tetapi kemudian kekuatan tekanan kolom air di atas Anda mulai membantu bergerak lebih dalam. Proses ini mengingatkan pada pembelajaran dan menyelam ke dalam sistem tipe TypeScript - itu menjadi sedikit lebih mudah saat Anda menyelam. Tetapi kita tidak boleh lupa untuk muncul tepat waktu.


Foto dari One Ocean One Breath .

Mikhail Bashurov (saitonakamura) - Senior Frontend Engineer di WiseBits, penggemar TypeScript dan freediver amatir. Analogi pembelajaran TypeScript dan menyelam dalam tidak disengaja. Michael akan memberi tahu Anda apa serikat yang didiskriminasi, bagaimana menggunakan inferensi jenis, mengapa Anda memerlukan kompatibilitas nominal dan pencitraan merek. Tahan napas dan selami.

Presentasi dan tautan ke GitHub dengan kode sampel di sini .

Jenis pekerjaan


Jenis-jenis jumlah terdengar aljabar - mari kita coba mencari tahu apa itu jumlah. Mereka disebut varian dalam ReasonML, tagged union di Haskell, dan diskriminasi union dalam F # dan TypeScript.

Wikipedia memberikan definisi berikut: "Jenis jumlah adalah jumlah dari jenis pekerjaan."
- Kapten terima kasih!

Definisi ini sepenuhnya benar, tetapi tidak berguna, jadi mari kita mengerti. Mari kita pergi dari pribadi dan mulai dengan jenis pekerjaan.

Misalkan kita memiliki tipe Task. Ini memiliki dua bidang: iddan siapa yang membuatnya.

type Task = { id: number, whoCreated: number }

Jenis pekerjaan ini adalah persimpangan atau persimpangan . Ini berarti bahwa kita dapat menulis kode yang sama dengan setiap bidang secara individual.

type Task = { id: number } & { whoCreated: number }

Dalam aljabar proposisi, ini disebut perkalian logis: ini adalah operasi "DAN" atau "DAN". Jenis produk adalah produk logis dari dua elemen ini.
Objek dengan seperangkat bidang dapat diekspresikan melalui produk logis.
Jenis pekerjaan tidak terbatas pada bidang. Bayangkan kita mencintai Prototipe dan memutuskan bahwa kita hilang leftPad.

type RichString = string & { leftPad: (toLength: number) => string }

Tambahkan ke String.prototype. Untuk mengekspresikan dalam tipe, kami mengambil string dan fungsi yang mengambil nilai yang diperlukan. Metode dan string adalah persimpangan.

Sebuah asosiasi


Gabungan - gabungan - berguna, misalnya, untuk mewakili tipe variabel yang menetapkan lebar elemen dalam CSS: string 10 piksel atau nilai absolut. Spesifikasi CSS sebenarnya jauh lebih rumit, tetapi untuk kesederhanaan, biarkan saja seperti itu.

type Width = string | number

Contohnya lebih rumit. Misalkan kita punya tugas. Itu bisa di tiga negara: baru saja dibuat, diterima dalam pekerjaan dan selesai.

type InitialTask = { id: number, whoCreated: number }
type InWorkTask = { id: number, whoCreated: number }
type FinishTask = { id: number, whoCreated: number, finishDate: Date }

type Task = InitialTask | InWorkTask | FinishedTask

Di negara bagian pertama dan kedua, tugas telah iddan siapa yang membuatnya, dan di negara ketiga, tanggal penyelesaian muncul. Di sini serikat muncul - baik ini atau itu.

Catatan . Sebagai gantinya, typeAnda bisa menggunakannya interface. Namun ada nuansa. Hasil gabungan dan persimpangan selalu merupakan tipe. Anda dapat mengekspresikan persimpangan melalui extends, tetapi penyatuan melalui interfacetidak bisa. Hanya typesaja lebih pendek.

interfacehanya membantu menggabungkan deklarasi. Jenis perpustakaan selalu perlu diekspresikan melalui interface, karena ini akan memungkinkan pengembang lain untuk memperluasnya dengan bidang mereka. Jadi ditandai, misalnya, jQuery-plugins.

Ketik kompatibilitas


Tetapi ada masalah - dalam TypeScript, kompatibilitas tipe struktural . Ini berarti bahwa jika tipe pertama dan kedua memiliki set bidang yang sama, maka ini adalah dua dari jenis yang sama. Misalnya, kami memiliki 10 jenis dengan nama yang berbeda, tetapi dengan struktur yang identik (dengan bidang yang sama). Jika kami menyediakan salah satu dari 10 jenis ke fungsi yang menerima salah satunya, TypeScript tidak akan keberatan.

Di Java atau C #, sebaliknya, sistem kompatibilitas nominal . Kami akan menulis 10 kelas yang sama dengan bidang yang identik dan fungsi yang mengambil salah satunya. Suatu fungsi tidak akan menerima kelas-kelas lain jika mereka bukan turunan langsung dari tipe yang dimaksudkan untuk fungsi tersebut.

Masalah kompatibilitas struktural ditangani dengan menambahkan status ke bidang: enumataustring. Tetapi tipe penjumlahan adalah cara untuk mengekspresikan kode secara semantik daripada bagaimana kode itu disimpan dalam database.

Sebagai contoh, kami akan alamat ke ReasonML. Ini adalah bagaimana jenis ini terlihat di sana.

type Task =
| Initial({ id: int, whoCreated: int })
| InWork({ id: int, whoCreated: int })
| Finished({
        id: int,
        whoCreated: int,
        finshDate: Date
    })

Kolom yang sama, kecuali itu intsebagai gantinya number. Jika Anda melihat dari dekat, kami perhatikan Initial, InWorkdan Finished. Mereka tidak terletak di sebelah kiri, dalam nama tipe, tetapi di sebelah kanan dalam definisi. Ini bukan hanya satu baris dalam judul, tetapi bagian dari tipe yang terpisah, sehingga kita dapat membedakan yang pertama dari yang kedua.

Retas seumur hidup . Untuk jenis umum (seperti entitas domain Anda) buat file global.d.tsdan tambahkan semua jenis ke dalamnya. Mereka akan secara otomatis terlihat di seluruh basis kode TypeScript tanpa perlu impor eksplisit. Ini nyaman untuk migrasi karena Anda tidak perlu khawatir tentang di mana menempatkan jenis.

Mari kita lihat bagaimana melakukan ini, menggunakan contoh kode Redux primitif.

export const taskReducer = (state, action) = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, erro: action.payload }
    }
    
    return state
}

Tulis ulang kode dalam TypeScript. Mari kita mulai dengan mengganti nama file - dari .jske .ts. Nyalakan semua opsi status dan opsi noImplicitAny. Opsi ini menemukan fungsi di mana tipe parameter tidak ditentukan, dan berguna pada tahap migrasi.

Dapat diketik State: tambahkan bidang isFetching, Task(yang tidak bisa) dan Error.

type Task = { title: string }

declare module "*.jpg" {
    const url: string
    export default url
}

declare module "*.png" {
    const url: string
    export default url
}

Catatan. Lifehack mengintip dalam TypeScript Deep Dive karya Basarat Ali Syed . Agak ketinggalan zaman, tetapi masih bermanfaat bagi mereka yang ingin terjun ke dalam TypeScript. Baca tentang keadaan TypeScript saat ini di blog Marius Schulz. Dia menulis tentang fitur dan trik baru. Pelajari juga log perubahan , tidak hanya informasi tentang pembaruan, tetapi juga cara menggunakannya.

Tindakan yang tersisa. Kami akan mengetik dan menyatakan masing-masing.

type FetchAction = {
    type: "TASK_FETCH"
}

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

type Actions = FetchAction | SuccessAction | FailAction

Bidang typeuntuk semua jenis berbeda, bukan hanya string, tetapi string spesifik - string literal . Ini adalah diskriminator yang sangat - tanda dimana kita membedakan semua kasus. Kami mendapat serikat yang didiskriminasi dan, misalnya, kami bisa memahami apa yang ada di payload.

Perbarui kode Redux asli:

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
    }
    
    return state
}

Ada bahaya di sini jika kita menulis string...
type FetchAction = {
    type: string
}
... maka semuanya akan pecah, karena tidak lagi diskriminatif.

Dalam tindakan ini, ketik bisa berupa string apa pun, dan bukan diskriminator tertentu. Setelah itu, TypeScript tidak akan dapat membedakan satu tindakan dari yang lain dan menemukan kesalahan. Oleh karena itu, harus ada string literal. Selain itu, kita dapat menambahkan desain ini: type ActionType = Actions["type"].

Tiga opsi kami akan muncul di prompt.



Jika Anda menulis ...
type FetchAction = {
    type: string
}
... maka petunjuknya akan sederhana string, karena semua baris lainnya tidak lagi penting.



Kita semua telah mengetik dan mendapatkan jenis jumlahnya.

Pemeriksaan menyeluruh


Bayangkan situasi hipotetis di mana penanganan kesalahan tidak ditambahkan ke kode.

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        //case "TASK_FAIL":
            //return { isFetching: false, error: action.payload }
    }
    
    return state
}

Pemeriksaan menyeluruh di sini akan membantu kami - properti lain yang bermanfaat seperti jumlah . Ini adalah kesempatan untuk memverifikasi bahwa kami telah memproses semua kasus yang mungkin.

Menambahkan variabel setelah pernyataan switch: const exhaustiveCheck: never = action.

tidak pernah merupakan tipe yang menarik:

  • jika itu adalah hasil dari suatu fungsi yang kembali, maka fungsi itu tidak pernah berakhir dengan benar (misalnya, ia selalu membuat kesalahan atau dieksekusi tanpa henti);
  • jika ini adalah tipe, maka tipe ini tidak dapat dibuat tanpa usaha ekstra.

Sekarang kompiler akan menunjukkan kesalahan "Ketik 'FailAction' tidak dapat ditugaskan untuk mengetik 'tidak pernah'". Kami memiliki tiga jenis tindakan yang mungkin, yang belum kami proses "TASK_FAIL"- ini FailAction. Tetapi untuk tidak pernah, itu tidak "ditugaskan".

Mari kita tambahkan pemrosesan "TASK_FAIL", dan tidak akan ada lagi kesalahan. Diproses "TASK_FETCH"- dikembalikan, diproses "TASK_SUCCESS"- dikembalikan, diproses "TASK_FAIL". Ketika kami memproses ketiga fungsi, apa yang bisa menjadi tindakan? Tidak ada - never.

Jika Anda menambahkan tindakan lain, kompiler akan memberi tahu Anda mana yang tidak diproses. Ini akan membantu jika Anda ingin merespons semua tindakan, dan jika hanya selektif, maka tidak.

Kiat pertama

Berusaha keras.
Pada awalnya akan sulit untuk menyelam: membaca tentang jenis penjumlahan, kemudian tentang produk, tipe data aljabar, dan seterusnya dalam rantai. Harus berusaha.

Ketika kita menyelam lebih dalam, tekanan kolom air di atas kita meningkat. Pada kedalaman tertentu, gaya apung dan tekanan air di atas kami seimbang. Ini adalah zona "daya apung netral", di mana kita berada dalam kondisi tanpa bobot. Dalam keadaan ini, kita dapat beralih ke sistem tipe lain atau bahasa.

Sistem Tipe Nominal


Di Roma kuno, penduduk memiliki tiga komponen nama: nama keluarga, nama dan "nama panggilan". Nama itu sendiri adalah "nomen." Dari sinilah muncul sistem jenis nominal - "nominal". Terkadang bermanfaat.

Sebagai contoh, kami memiliki kode fungsi seperti itu.

export const githubUrl = process.env.GITHUB_URL as string
export const nodeEnv = process.env.NODE_ENV as string

export const fetchStarredRepos = (
    nodeEnv: string,
    githubUrl: string
): Promise<GithubRepo[ ]> => {
    if (nodeEnv = "production") {
        // log call
    }

    // prettier-ignore
    return fetch('$githubUrl}/users/saitonakamura/starred')
        .then(r => r.json());
}

Konfigurasi datang pada API: GITHUB_URL=https://api.github.com. Metode githubUrlAPI mengeluarkan repositori tempat kami meletakkan tanda bintang. Dan jika nodeEnv = "production", maka ia mencatat panggilan ini, misalnya, untuk metrik.

Untuk fungsi yang ingin kita kembangkan (UI).

import React, { useState } from "react"

export const StarredRepos = () => {
    const [starredRepos, setStarredRepos] = useState<GithubRepo[ ] | null>(null)

    if (!starredRepos) return <div>Loading…</div>

    return (
        <ul>
            {starredRepos.map(repo => (
                <li key={repo.name}>repo.name}</li>
            ))}
        </ul>
    )
}

type GithubRepo = {
    name: string
}

Fungsi sudah tahu cara menampilkan data, dan jika tidak ada, maka loader. Tetap menambahkan panggilan API dan mengisi data.

useEffect(() => {
    fetchStarredRepos(githubUrl, nodeEnv).then(data => setStarredRepos(data))
}, [ ])

Tetapi jika kita menjalankan kode ini, semuanya akan jatuh. Di panel pengembang, kami menemukan bahwa ia fetchmengakses alamat '/users/saitonakamura/starred'- githubUrl telah menghilang di suatu tempat. Ternyata semua itu karena desain yang aneh - yang nodeEnvlebih dulu. Tentu saja, mungkin tergoda untuk mengubah segalanya, tetapi fungsinya dapat digunakan di tempat lain dalam basis kode.

Tetapi bagaimana jika kompiler meminta ini sebelumnya? Maka Anda tidak harus melalui seluruh siklus start-up, mendeteksi kesalahan, mencari alasannya.

Branding


TypeScript memiliki retasan untuk ini - tipe merek. Buat Brand, B(string), Tdan dua jenis. Kami akan membuat jenis yang sama untuk NodeEnv.

type Brand<T, B extends string> = T & { readonly _brand: B }

type GithubUrl = Brand<string, "githubUrl">
export const githubUrl = process.env.GITHUB_URL as GithubUrl

type NodeEnv = Brand<string, "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

Tapi kami mendapat kesalahan.



Sekarang githubUrltidak nodeEnvmungkin untuk menetapkan satu sama lain, karena ini adalah jenis nominal. Tidak mengganggu, tetapi nominal. Sekarang kita tidak dapat menukar mereka di sini - kita mengubahnya di bagian lain dari kode.

useEffect(() => {
    fetchStarresRepos(nodeEnv, githubUrl).then(data => setStarredRepos(data))
}, [ ])

Sekarang semuanya baik-baik saja - mereka mendapat primitif bermerek . Branding berguna ketika beberapa argumen (string, angka) ditemukan. Mereka memiliki semantik tertentu (koordinat x, y), dan mereka tidak perlu bingung. Lebih mudah ketika kompiler memberi tahu Anda bahwa mereka bingung.

Namun ada dua masalah. Pertama, TypeScript tidak memiliki tipe nominal asli. Tetapi ada harapan bahwa itu akan diselesaikan, diskusi tentang masalah ini sedang berlangsung di repositori bahasa .

Masalah kedua adalah "sebagai", tidak ada jaminan bahwa GITHUB_URLtautan tersebut tidak rusak.

Serta dengan NODE_ENV. Kemungkinan besar, kami ingin bukan hanya beberapa string, tetapi "production"atau "development".

type NodeEnv = Brand<"production" | "development" | "nodeEnv">
export const nodeEnv = process.env.NODE_ENV as NodeEnv

Semua ini perlu diperiksa. Saya merujuk Anda ke desainer cerdas dan laporan Sergey Cherepanov, " Merancang Domain dengan TypeScript dalam Gaya Fungsional ."

Kiat kedua dan ketiga

Hati-hati.
Terkadang berhenti dan melihat-lihat: di bahasa lain, kerangka kerja, sistem ketik. Pelajari prinsip-prinsip baru dan pelajari pelajaran.

Ketika kita melewati titik "daya apung netral", air menekan lebih keras dan menarik kita ke bawah.
Bersantai.
Selami lebih dalam dan biarkan TypeScript melakukan tugasnya.

Apa yang dapat dilakukan TypeScript


TypeScript dapat mencetak tipe .

export const createFetch = () => ({
    type: "TASK_FETCH"
})

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
})

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
})

type FetchAction = {
    type: "TASK_FETCH",

type SuccessAction = {
    type: "TASK_SUCCESS",
    payload: Task

type FailAction = {
    type: "TASK_FAIL",
    payload: Error
}

Ada TypeScript untuk ini ReturnType- ini mendapatkan nilai balik dari fungsi:
type FetchAction = ReturnType<typeof createFetch>
Di dalamnya kita melewati jenis fungsi. Kita tidak bisa menulis fungsi: untuk mengambil tipe dari fungsi atau variabel, kita perlu menulis typeof.

Kami lihat di tips type: string.



Ini buruk - pembeda akan pecah karena ada objek literal.

export const createFetch = () => ({
    type: "TASK_FETCH"
})

Ketika kita membuat objek dalam JavaScript, itu bisa berubah secara default. Ini berarti bahwa dalam objek dengan bidang dan string, kita nanti dapat mengubah string ke yang lain. Oleh karena itu, TypeScript memperluas string tertentu ke string apa pun untuk objek yang bisa diubah.

Kami perlu membantu TypeScript entah bagaimana. Ada sebagai const untuk ini.

export const createFetch = ( ) => ({
    type: "TASK_FETCH" as const
})

Tambah - string akan segera menghilang di konfirmasi. Kita dapat menulis ini tidak hanya melintasi garis, tetapi secara umum seluruh literal.

export const createFetch = ( ) => ({
    type: "TASK_FETCH"
} as const)

Maka tipe (dan semua bidang) akan menjadi hanya baca.

type FetchAction = {
    readonly type: "TASK_FETCH";
}

Ini berguna karena Anda tidak mungkin mengubah kebobolan tindakan Anda. Karena itu, kami menambahkan sebagai const di mana-mana.

export const createFetch = () => ({
    type: "TASK_FETCH"
} as const)

export const createSuccess = (task: Task) => ({
    type: "TASK_SUCCESS"
    payload: Task
} as const)

export const createFail = (error: Error) => ({
    type: "TASK_FAIL"
    payload: error
} as const)

type Actions =
    | ReturnType<typeof createFetch>
    | ReturnType<typeof createSuccess>
    | ReturnType<typeof createFail>

type State =
    | { isFetching: true }
    | { isFetching: false; task: Task }
    | { isFetching: false; error: Error }

export const taskReducer = (state: State, action: Actions): State = > {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

  const _exhaustiveCheck: never = action

  return state
}

Semua tindakan mengetik kode telah dikurangi dan ditambahkan sebagai const. TypeScript mengerti segalanya.

TypeScript dapat menampilkan Status . Itu diwakili oleh gabungan dalam kode di atas dengan tiga kemungkinan status isFetching: true, false, atau Task.

Gunakan tipe State = ReturnType. Petunjuk TypeScript menunjukkan bahwa ada ketergantungan melingkar.



Mempersingkat.

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

Stateberhenti mengutuk, tetapi sekarang dia any, karena kita memiliki ketergantungan siklus. Kami mengetik argumennya.

type State = ReturnType<typeof taskReducer>

export const taskReducer = (state: { isFetching: true }, action: Actions) => {
    switch (action.type) {
        case "TASK_FETCH":
            return { isFetching: true }
        case "TASK_SUCCESS":
            return { isFetching: false, task: action.payload }
        case "TASK_FAIL":
            return { isFetching: false, error: action.payload }
}

const _exhaustiveCheck: never = action

return state
}

Kesimpulannya sudah siap.



Kesimpulan ini mirip dengan apa yang kita miliki awalnya: true, false, Task. Ada ladang sampah di sini Error, tetapi dengan jenis undefined- bidang tampaknya ada di sana, tetapi sepertinya tidak.

Tip keempat

Jangan terlalu banyak bekerja.
Jika Anda bersantai dan menyelam terlalu dalam, mungkin tidak ada cukup oksigen untuk kembali.

Pelatihan juga: jika Anda terlalu tenggelam dalam teknologi dan memutuskan untuk menerapkannya di mana-mana, maka kemungkinan besar Anda akan menemukan kesalahan, alasan yang tidak Anda ketahui. Ini akan menyebabkan penolakan dan tidak lagi ingin menggunakan tipe statis. Cobalah untuk mengevaluasi kekuatan Anda.

Bagaimana TypeScript Memperlambat Pengembangan


Butuh waktu untuk mendukung pengetikan. Itu tidak memiliki UX terbaik - kadang-kadang memberikan kesalahan yang benar-benar tidak dapat dipahami, seperti sistem tipe lainnya, seperti di Flow atau Haskell.
Semakin ekspresif sistem, semakin sulit kesalahannya.
Nilai dari sistem tipe adalah bahwa ia memberikan umpan balik cepat ketika kesalahan terjadi. Sistem akan menampilkan kesalahan dan membutuhkan waktu lebih sedikit untuk menemukan dan memperbaikinya. Jika Anda menghabiskan lebih sedikit waktu untuk memperbaiki kesalahan, maka lebih banyak solusi arsitektur akan menerima lebih banyak perhatian. Tipe tidak memperlambat pengembangan jika Anda belajar untuk bekerja dengannya.

++. - , , (25 26 ) - (27 — 10 ).

(5900 ), ++ IT-, .

AvitoTech FrontendConf 2019 . youtube- telegram @FrontendConfChannel, .

All Articles