Typescript Narrowing Patterns Discriminated Unions Exhaustive Checks Runtime Validation Basics
Selesai membaca dokumentasi ini?
Kembali ke rujukan utama TypeScript untuk melanjutkan topik lainnya.
Kembali ke rujukan utama TypeScript untuk melanjutkan topik lainnya.
TypeScript memberi tipe statis saat compile time, tapi program tetap berjalan di runtime dengan data yang bisa datang dari API, form, storage, event, atau third-party library. Karena itu, satu tipe yang terlihat aman di editor belum tentu benar saat aplikasi benar-benar jalan.
Topik ini membahas empat hal yang paling sering dipakai untuk menjembatani compile time dan runtime:
unknown menjadi tipe yang lebih spesifik.kind atau status.Kalau dipakai bareng, keempatnya membuat kode TypeScript lebih aman, lebih mudah dibaca, dan lebih tahan terhadap data yang tidak sesuai kontrak.
Masalah paling umum di TypeScript bukan kurang tipe, melainkan terlalu percaya pada tipe.
Contoh klasik:
User, padahal server mengirim payload yang salah;localStorage disimpan sebagai JSON, lalu dibaca ulang tanpa validasi;URLSearchParams dianggap sudah benar;TypeScript tidak memeriksa data saat runtime. Dia hanya memeriksa apakah kode kamu konsisten dengan tipe yang kamu tulis. Jadi, kalau sumber data di luar kontrolmu, kamu tetap perlu validasi runtime.
Narrowing adalah proses mempersempit tipe dari bentuk yang lebih luas ke bentuk yang lebih spesifik berdasarkan kondisi runtime.
Narrowing biasanya terjadi lewat:
typeofinstanceofinif, switch, dan returnType guard adalah pengecekan runtime yang juga dipahami TypeScript untuk mempersempit tipe.
typeofPakai untuk tipe primitif.
function formatInput(value: string | number) {
if (typeof value === "string") {
return value.trim().toLowerCase();
}
return value.toFixed(2);
}Catatan:
typeof null adalah "object", jadi jangan dipakai sendirian untuk object validation.typeof berguna untuk membedakan string, number, boolean, bigint, symbol, function, dan undefined.instanceofPakai untuk class instance atau built-in object tertentu.
function toMessage(error: unknown) {
if (error instanceof Error) {
return error.message;
}
return "Unknown error";
}Catatan:
instanceof bergantung pada prototype chain.instanceof biasanya tidak membantu karena hasil parse bukan instance class.inPakai untuk mengecek apakah sebuah properti ada di object.
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(pet: Cat | Dog) {
if ("meow"
Catatan:
in berguna untuk union object yang berbeda shape.Custom type guard adalah function yang return type-nya memakai predikat value is T.
type User = {
id: string;
name: string;
};
function isUser(value: unknown): value is User {
if (
Custom guard bagus kalau:
Assertion function cocok kalau kamu ingin melempar error saat data tidak valid.
function assertUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error("Invalid user payload");
}
}Bedanya dengan type guard biasa:
if.Discriminated union adalah union object yang punya field pembeda stabil, misalnya kind, type, status, atau state.
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; message: string
Field status adalah discriminator. Karena nilainya literal dan konsisten, TypeScript bisa mempersempit tipe secara akurat.
function renderState(state: State) {
if (state.status === "loading") {
return "Loading...";
}
if (state.status === "success") {
return state.data.join(", ");
}
status untuk state requestkind untuk event atau commandtype untuk actionstep untuk flow multi-langkahKalau union mulai terasa besar, pecah berdasarkan domain atau pisahkan state machine yang lebih spesifik.
Exhaustive check memastikan semua variant union ditangani. Ini penting karena kalau ada variant baru ditambahkan nanti, compiler harus membantu menemukan cabang yang belum diperbarui.
switch exhaustive dengan neverfunction assertNever(value: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}
function renderStateExhaustive(state
Kalau ada variant baru ditambahkan ke State, default akan mulai error karena state tidak lagi bertipe never.
never pentingnever menandakan cabang yang secara logika tidak mungkin terjadi. Jadi saat compiler masih melihat kemungkinan lain, itu artinya handling kamu belum lengkap.
switchKalau tidak memakai switch, kamu masih bisa membuat exhaustive check dengan chain if/else dan assertNever di akhir. Prinsipnya sama: semua branch harus habis.
Runtime validation dipakai untuk memastikan data dari luar benar-benar sesuai shape yang diharapkan sebelum dipakai sebagai tipe domain.
localStorage atau sessionStorageunknown, bukan langsung tipe domain;function parseUser(raw: unknown): User | null {
if (!isUser(raw)) {
return null;
}
return raw;
}Pola yang umum adalah memakai schema validator seperti Zod, Valibot, Yup, atau library sejenis untuk:
Contoh bentuk pola, tanpa bergantung ke library tertentu:
// schema -> validate -> inferred type
const result = validateUserPayload(payload);
if (!result.ok) {
throw new Error(result.error);
}
const user = result.data; // sudah tervalidasiconst user = payload as User;Ini hanya memberi tahu compiler untuk percaya, bukan memvalidasi data. Kalau payload salah, bug-nya tetap muncul di runtime.
JSON.parse() mengembalikan any secara historis atau unknown tergantung wrapper yang dipakai, tapi intinya hasil parse tetap harus divalidasi sebelum dipakai.
as dipakai untuk menutup error alih-alih memperbaiki modelKalau kamu sering perlu as SomeType, biasanya ada masalah di boundary data, bukan di TypeScript-nya.
type Result = {
data?: string;
};data belum tentu ada. Kalau dipakai tanpa guard, branch runtime bisa gagal walau compiler tidak langsung protes karena ada jalur yang tampak aman.
Misalnya cek satu field saja padahal field lain juga wajib. TypeScript akan percaya kalau custom guard kamu bilang lolos, jadi isi guard harus benar-benar ketat.
Kalau shape banyak tapi tidak ada pembeda yang jelas, narrowing jadi berantakan dan kode biasanya berakhir penuh as.
unknown.switch atau if berbasis discriminator.unknown untuk data yang belum dipercaya.as sebelum validasi.switch + never untuk exhaustive check.Types, Interfaces, Generics, Narrowing, dan Utility Types di TypeScriptAdvanced Types TypeScript: Unions, Intersections, Mapped Types, Conditional Types, infer, dan Type GuardsPHP Validation, Form Requests, Redirects, dan Error Bags untuk pola validasi di layer serverFetch API, Async Data Flow, JSON Parsing, dan Error Handling Dasar untuk boundary data di frontend