Implementasi

Mengerjakan proyek dalam strict mode, tanpa any, dengan tradeoff terdokumentasi.

Daftar isi

Fungsi pustaka yang dipakai orang lain wajib tahan terhadap masukan yang tidak sah, dan keketatan compiler harus menutupi seluruh badan pustaka, bukan hanya pemanggilnya. Topik ini mengerjakan fungsi-fungsi pustaka-waktu dalam strict mode tanpa satu pun any, mendokumentasikan setiap tradeoff yang diambil, dan menutup pekerjaannya dengan pemeriksaan penuh yang berakhir bersih. Program uji memperlihatkan kedua sisi perilaku, yaitu jalur normal yang menghasilkan nilai dan jalur penolakan yang melempar error. Setelah topik ini selesai, pustaka kecil sudah memenuhi standar yang kelak dituntut oleh gerbang CI.

Menyalakan strict pada proyek#

Keketatan pemeriksaan dinyatakan pada tsconfig.json di akar folder pustaka. Opsi strict menyalakan keluarga pemeriksaan ketat sekaligus, sedangkan noEmit membuat tsc hanya berperan sebagai pemeriksa karena pustaka dijalankan langsung dengan node yang melakukan penghapusan tipe. Opsi allowImportingTsExtensions diperlukan karena seluruh impor memakai akhiran .ts, dan include membatasi lingkup pemeriksaan pada folder src beserta berkas ujinya. Berkas kontrak src/tipe-publik.ts tetap menjadi bagian proyek dan ikut diperiksa karena berada di dalam lingkup include.

json pustaka-waktu/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "nodenext",
    "target": "esnext",
    "allowImportingTsExtensions": true
  },
  "include": ["src/**/*.ts", "uji.ts"]
}

Pemilihan konfigurasi ini membuat pemeriksaan berjalan pada tingkat yang sama dengan kode produksi. Strict bukan sekadar opsi tambahan, melainkan syarat yang membuat anotasi tipe benar-benar bekerja pada seluruh bagian berkas, termasuk variabel yang dibiarkan tanpa anotasi. Opsi target dan module menyesuaikan bentuk program dengan Node.js yang menjalankannya secara langsung. Konfigurasi yang sama dipakai ulang oleh gerbang CI, sehingga tidak ada perbedaan keketatan antara mesin pengembang dan mesin pemeriksa.

Menolak masukan tidak sah#

Isi src/konversi.ts kini memuat keputusan-keputusan pengerjaan yang paling menentukan. Fungsi pembantu jagaDurasi ditulis sebagai fungsi internal yang tidak diekspor, dan setiap fungsi publik memanggilnya lebih dahulu sebelum menghitung. Kebijakan yang dipilih berupa menolak, bukan menormalkan, karena durasi 0 mempunyai makna sendiri pada jadwal dan tidak boleh dipakai untuk menutupi masukan yang rusak.

ts pustaka-waktu/src/konversi.ts
// tidak-dijalankan
import { type SatuanDurasi } from "./tipe-publik.ts";

// tradeoff: masukan tidak sah ditolak dengan error, bukan dinormalkan menjadi 0,
// karena durasi 0 pada jadwal mempunyai makna berbeda dari masukan yang rusak.
function jagaDurasi(nilai: number): void {
  if (Number.isNaN(nilai) || nilai < 0) {
    throw new RangeError("durasi tidak boleh NaN atau negatif");
  }
}

export function menitKeJam(menit: number): number {
  jagaDurasi(menit);
  return menit / 60;
}

export function jamKeMenit(jam: number): number {
  jagaDurasi(jam);
  return jam * 60;
}

export function keMenit(nilai: number, dari: SatuanDurasi): number {
  jagaDurasi(nilai);
  if (dari === "jam") {
    return nilai * 60;
  }
  if (dari === "menit") {
    return nilai;
  }
  const sisa: never = dari;
  throw new Error(`satuan tidak dikenal: ${String(sisa)}`);
}

export function pilihTerpanjang<T extends { menit: number }>(daftar: readonly T[]): T | undefined {
  let terpilih: T | undefined;
  for (const item of daftar) {
    jagaDurasi(item.menit);
    if (terpilih === undefined || item.menit > terpilih.menit) {
      terpilih = item;
    }
  }
  return terpilih;
}

// tradeoff: masukan dari luar pustaka diterima sebagai unknown, bukan any, sehingga
// pemanggil dipaksa melakukan narrowing sebelum nilai durasinya dipakai.
export function angkaAman(nilai: unknown): number {
  if (typeof nilai === "number" && Number.isFinite(nilai) && nilai >= 0) {
    return nilai;
  }
  throw new TypeError("masukan harus berupa angka durasi berhingga dan tak negatif");
}

Fungsi keMenit memperlihatkan pola masukan union yang kemudian dinormalkan. Parameter dari bertipe SatuanDurasi, dan percabangan menyaring satuannya satu per satu. Baris const sisa: never = dari hanya lolos kompilasi bila seluruh anggota union sudah ditangani, sehingga penambahan satuan baru pada masa depan memaksa fungsi ini diperbarui sebelum pemeriksaan lolos.

Fungsi pilihTerpanjang memperlihatkan generic terbatas, yaitu parameter tipe yang dibatasi pada bentuk minimum yang dipakai fungsinya. Batasannya berupa { menit: number }, sehingga fungsi ini menerima objek apa pun yang punya field menit tanpa mengetahui field lainnya, dan tipe objek aslinya tetap utuh pada nilai kembalian. Fungsi angkaAman menutup bagian ini dengan pola pengganti any, yaitu menerima unknown lalu mempersempitnya melalui typeof sebelum nilai dipakai.

Format dengan gerbang yang sama#

src/format.ts memakai kebijakan penolakan yang sama sebelum menyusun teks. Pemanggilan formatDurasi dengan NaN atau nilai negatif melempar RangeError, dan pemeriksaan itu ditempatkan pada baris pertama fungsi agar tidak ada jalur yang melewatinya. Kebijakan ini sama pentingnya dengan penolakan pada fungsi konversi, karena teks durasi yang menyamar juga menutupi masukan yang rusak. Pesan errornya menyebut kata durasi agar penyebab penolakan langsung terbaca tanpa membaca kodenya. Parameter opsi tetap opsional sesuai kontrak publik, dan nilai opsinya diakses setelah keberadaannya diperiksa.

ts pustaka-waktu/src/format.ts
// tidak-dijalankan
import { type OpsiFormatDurasi } from "./tipe-publik.ts";

export function formatDurasi(menit: number, opsi?: OpsiFormatDurasi): string {
  if (Number.isNaN(menit) || menit < 0) {
    throw new RangeError("durasi tidak boleh NaN atau negatif");
  }
  const jam = Math.floor(menit / 60);
  const sisa = menit % 60;
  if (opsi === undefined) {
    if (jam === 0) {
      return `${sisa} menit`;
    }
    if (sisa === 0) {
      return `${jam} jam`;
    }
    return `${jam} jam ${sisa} menit`;
  }
  if (opsi.satuan === "jam") {
    return `${(menit / 60).toFixed(opsi.angkaDesimal)} jam`;
  }
  return `${menit} menit`;
}

Cabang opsi === undefined menjaga pemanggil lama, sedangkan cabang opsi.satuan === "jam" menyaring satuan sebelum memformat angka desimalnya. Penataan cabang semacam ini membuat compiler mampu menegakkan setiap akses field karena bentuk OpsiFormatDurasi sudah terjamin pada titik aksesnya. Seluruh fungsi pustaka dengan demikian membaca masukan yang sama, yaitu jumlah menit, dan menolak nilai tidak sah sebelum menghitung apa pun. Tidak ada satu pun akses properti yang berada di luar jangkauan pemeriksaan tipe.

Program uji dan pemeriksaan penuh#

Program uji menutup pekerjaan implementasi dengan membuktikan kedua sisi perilaku. Sisi pertama berupa jalur normal yang mencetak hasil pemformatan, konversi, dan pemilihan kegiatan terlama. Sisi kedua berupa jalur penolakan yang menangkap error lalu mencetak pesannya, sehingga kebijakan menolak masukan benar-benar terbukti dan bukan hanya dinyatakan.

ts pustaka-waktu/uji.ts
// tidak-dijalankan
import { angkaAman, keMenit, menitKeJam, pilihTerpanjang } from "./src/konversi.ts";
import { formatDurasi } from "./src/format.ts";

console.log(formatDurasi(90));
console.log(formatDurasi(90, { satuan: "jam", angkaDesimal: 2 }));
console.log(keMenit(1.5, "jam"));
console.log(menitKeJam(135));

const kegiatan = [
  { nama: "menulis", menit: 45 },
  { nama: "membaca", menit: 70 },
  { nama: "olahraga", menit: 30 },
];
const terpilih = pilihTerpanjang(kegiatan);
console.log(terpilih?.nama, terpilih?.menit);

const masukanJson: unknown = JSON.parse("90");
console.log(keMenit(angkaAman(masukanJson), "menit"));

const masukanRusak: unknown = JSON.parse('"banyak"');
try {
  console.log(keMenit(angkaAman(masukanRusak), "menit"));
} catch (error) {
  console.log("ditolak:", (error as Error).message);
}

try {
  console.log(formatDurasi(Number.NaN));
} catch (error) {
  console.log("ditolak:", (error as Error).message);
}
bash
# menjalankan program uji yang memuat jalur normal dan jalur penolakan
node --no-warnings pustaka-waktu/uji.ts
text Keluaran
1 jam 30 menit
1.50 jam
90
2.25
membaca 70
90
ditolak: masukan harus berupa angka durasi berhingga dan tak negatif
ditolak: durasi tidak boleh NaN atau negatif

Keluaran tersebut memperlihatkan bahwa angkaAman menolak teks “banyak” yang keluar dari JSON dengan TypeError, dan formatDurasi menolak NaN dengan RangeError. Kegiatan “membaca” yang berdurasi 70 menit terpilih sebagai yang terpanjang tanpa kehilangan field namanya. Pemanggilan JSON.parse("90") yang sah lolos dan dinormalkan menjadi 90 menit.

Pemeriksaan penuh proyek menjadi langkah penutup. Perintah berikut memeriksa seluruh berkas pada include tanpa menghasilkan berkas apa pun, dan berakhirnya tanpa pesan menandakan pustaka lolos strict mode secara utuh. Pemanggilannya memakai opsi –project agar keketatannya persis seperti yang dinyatakan pada tsconfig.json.

bash
# menjalankan pemeriksaan penuh proyek dalam strict mode
tsc --project pustaka-waktu --pretty false

Pemeriksaan berakhir tanpa satu pun pesan, dan keheningan inilah tanda lolosnya gerbang typecheck. Seluruh fungsi telah dikerjakan tanpa any, seluruh tradeoff terdokumentasi sebagai komentar di tempat keputusannya diambil, dan program uji membuktikan perilakunya dengan keluaran nyata.

Kesalahan umum#

  • Memakai any untuk menutup error compiler dengan cepat. Error yang ditelan tersebut berpindah menjadi error saat program berjalan, biasanya pada pemanggil yang tidak bersalah. Nilai bertipe keliru pun lolos tanpa satu pun peringatan. Perbaikannya berupa menerima unknown lalu mempersempitnya, atau menganotasi bentuk masukannya dengan benar.
  • Menormalkan masukan rusak menjadi 0 agar program tetap jalan. Nilai 0 menyamar sebagai durasi sah dan menyembunyikan sumber datanya yang rusak. Masalah yang seharusnya tertangkap di gerbang masukan justru menjalar ke bagian lain program. Perbaikannya berupa melempar error dengan pesan yang jelas seperti pada jagaDurasi.
  • Mengandalkan language server sebagai satu-satunya pemeriksa. Editor memeriksa berkas yang terbuka, sedangkan berkas lain yang lupa disunting tetap lolos. Berkas baru yang belum pernah dibuka di editor pun tak tersentuh pemeriksaan. Perbaikannya berupa menjalankan tsc --project yang memeriksa seluruh include sebelum pekerjaan dinyatakan selesai.

Ringkasan#

  • Strict mode dinyatakan pada tsconfig.json, dan pemeriksaan penuh berupa tsc --project pustaka-waktu --pretty false yang berakhir tanpa pesan.
  • Masukan tidak sah ditolak dengan error bertipe jelas, dan keputusan menolak alih-alih menormalkan didokumentasikan sebagai komentar tradeoff.
  • Pola pengganti any yang dipakai berupa unknown dengan narrowing pada angkaAman dan generic terbatas pada pilihTerpanjang.
  • Baris const sisa: never = dari menjadikan percabangan satuan ekshaustif, sehingga penambahan satuan baru ditolak compiler sampai ditangani.

Referensi#