Typescript Advanced Types Unions Intersections Mapped Conditional Infer Type Guards
Definisi singkat
Advanced types di TypeScript dipakai untuk membentuk tipe yang lebih ekspresif dari sekadar object shape dasar. Di sini, tipe bukan cuma dipakai untuk memberi label struktur data, tapi juga untuk menggabungkan, memetakan, menyaring, dan menurunkan tipe lain secara komposisional.
Lima konsep yang paling sering muncul dalam kerja harian adalah:
- union: satu nilai bisa punya salah satu dari beberapa tipe.
- intersection: satu nilai harus memenuhi beberapa tipe sekaligus.
- mapped types: membangun tipe baru dengan mengiterasi properti dari tipe lain.
- conditional types: memilih tipe berdasarkan kondisi.
infer: mengekstrak bagian dari tipe lain di dalam conditional type.- type guards: mengecilkan tipe union saat runtime lewat pengecekan yang bisa dipahami TypeScript.
Kalau dipakai dengan tepat, keenamnya membantu membuat tipe yang:
- reusable,
- konsisten,
- dan lebih dekat ke aturan domain asli.
Kapan advanced types dipakai
Pakai advanced types saat tipe yang kamu butuhkan tidak bisa dijelaskan dengan bentuk statis yang sederhana.
Gunakan union kalau
- sebuah nilai bisa punya beberapa variasi valid,
- kamu ingin memodelkan status, mode, atau pilihan,
- logika runtime memang bercabang.
Contoh umum:
type RequestState = 'idle' | 'loading' | 'success' | 'error';Gunakan intersection kalau
- kamu ingin menggabungkan beberapa kontrak tipe,
- satu objek harus punya atribut dari beberapa sumber,
- kamu sedang membangun tipe komposit dari beberapa layer.
Contoh umum:
type WithId = { id: string };
type WithTimestamps = { createdAt: string; updatedAt: string };
type UserRecord = WithId & WithTimestamps;Gunakan mapped types kalau
- kamu ingin membuat variasi dari tipe object yang sudah ada,
- kamu butuh transformasi massal atas property,
- kamu ingin menghindari duplikasi antara model inti dan model turunan.
Gunakan conditional types kalau
- output tipe bergantung pada input tipe,
- kamu ingin menyaring tipe tertentu dari union,
- kamu ingin membuat helper tipe yang adaptif.
Gunakan infer kalau
- kamu perlu mengambil bagian internal dari tipe lain,
- kamu ingin mengekstrak return type, item type, atau parameter type,
- kamu sedang membuat utility type yang meniru perilaku built-in TypeScript.
Gunakan type guards kalau
- union harus dipersempit berdasarkan data runtime,
- kamu memproses
unknown, - kamu ingin logika narrowing yang bisa dipakai ulang di banyak tempat.
Cara kerja komposisi tipe
Inti kekuatan advanced types ada di komposisi. Satu tipe bisa menjadi bahan baku untuk tipe lain, lalu hasilnya dipakai lagi sebagai input.
Pola ini penting karena kamu tidak perlu menulis ulang bentuk data yang sama di banyak tempat. Yang berubah cukup satu sumber kebenaran, lalu tipe turunan mengikuti.
Union: memodelkan variasi valid
Union dipakai saat sebuah nilai bisa berupa salah satu dari beberapa tipe.
type Input = string | number;Kapan union tepat
- input form bisa string atau number,
- response API punya beberapa status shape,
- event atau command punya banyak variant.
Kelebihan union
- representasi domain lebih jujur,
- lebih aman daripada
any, - memaksa kamu menangani semua cabang penting.
Contoh discriminated union
type Loading = { status: 'loading' };
type Success = { status: 'success'; data: string[] };
type ErrorState = { status: 'error'; message: string };
type State = Loading | Success | ErrorState;Discriminated union biasanya lebih baik daripada union biasa karena TypeScript bisa mengenali pembeda yang stabil, misalnya status.
Jebakan union
- union yang terlalu lebar membuat narrowing jadi sulit,
- properti yang tidak ada di semua anggota union tidak bisa diakses langsung,
- kalau terlalu banyak variant, kode cabang bisa cepat meledak.
Kalau union mulai terasa berat, biasanya kamu perlu discriminator yang lebih rapi atau pecah model domain-nya.
Intersection: menggabungkan kontrak
Intersection A & B berarti nilai harus memenuhi semua sisi sekaligus.
type Entity = { id: string } & { createdAt: string };Kapan intersection tepat
- hasil gabungan data dari dua sumber,
- wrapper yang menambah metadata,
- reuse contract dari layer berbeda.
Contoh praktis
type ApiMeta = {
requestId: string;
receivedAt: string;
};
type User = {
id: string;
name: string;
};
type UserResponse = User & ApiMeta;Kelebihan intersection
- enak untuk komposisi,
- mengurangi duplikasi field,
- cocok untuk tipe “hasil tambah” dari beberapa sumber.
Jebakan intersection
- kalau dua tipe punya properti yang sama tapi tidak kompatibel, hasilnya bisa menjadi tidak usable,
- intersection tidak berarti “gabungan longgar”; ia bisa jadi sangat ketat,
- terlalu banyak intersection berlapis membuat tipe susah dibaca.
Mapped types: transformasi berbasis properti
Mapped types membentuk tipe baru dengan iterasi atas property keys dari tipe lain.
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};Kapan mapped types dipakai
- membuat versi
readonly,partial, ataunullabledari model, - menyalin struktur object sambil mengubah atributnya,
- membangun helper tipe untuk form, patch, atau state update.
Contoh transformasi
type FormState<T> = {
[K in keyof T]: {
value: T[K];
touched: boolean;
error?: string;
};
};Dengan pola ini, kamu cukup mendefinisikan model inti sekali, lalu form state mengikuti otomatis.
Modifier yang sering dipakai
type DeepReadonlyish<T> = {
readonly [K in keyof T]?: T[K];
};Kamu bisa menambah atau menghapus modifier seperti readonly dan ? untuk membangun variasi tipe yang lebih spesifik.
Jebakan mapped types
- nested object tidak otomatis ikut berubah kalau kamu tidak membuat versi recursive,
- key yang sangat banyak bisa membuat hasil tipe sulit dibaca di editor,
- transformasi yang terlalu pintar sering sulit dipahami anggota tim lain.
Conditional types: memilih tipe berdasarkan kondisi
Conditional types memakai bentuk:
type Result<T> = T extends SomeType ? A : B;Kapan conditional types dipakai
- kamu ingin tipe hasil bergantung pada input,
- kamu ingin menyaring union,
- kamu ingin membuat helper yang adaptif terhadap tipe yang masuk.
Contoh sederhana
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // falseDistribusi atas union
Salah satu perilaku penting: conditional types sering terdistribusi ke union.
type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>; // string[] | number[]Ini berguna, tapi juga sering bikin hasil akhir berbeda dari dugaan awal.
Jebakan conditional types
- hasil bisa meledak jadi terlalu kompleks,
- distribusi union kadang bikin tipe sulit diprediksi,
- nested conditional type sulit dibaca kalau tidak diberi nama helper.
Kalau logic-nya mulai rumit, pecah menjadi beberapa alias kecil. Itu lebih mudah dirawat daripada satu tipe raksasa.
infer: mengekstrak bagian dari tipe lain
infer dipakai di dalam conditional type untuk menangkap bagian dari tipe yang sedang dicocokkan.
Contoh return type
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;Contoh extract item type
type ItemType<T> = T extends (infer U)[] ? U : never;
type A = ItemType<string[]>; // stringKapan infer berguna
- menulis utility type custom,
- mengambil return type, parameter type, atau element type,
- membuat tipe turunan tanpa hardcode bentuk finalnya.
Jebakan infer
- terlalu banyak
inferdalam satu tipe membuat pembaca kehilangan jejak, - nama alias yang tidak deskriptif bikin maksud helper sulit dipahami,
- hasil
neversering muncul diam-diam kalau pattern tidak cocok.
Type guards: menghubungkan tipe dengan runtime
Type guards dipakai saat runtime untuk memberi petunjuk ke TypeScript bahwa suatu nilai sudah dipersempit.
Guard dasar dengan typeof
function format(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}Guard dengan operator in
type User = { id: string; name: string };
type Admin = { id: string; role: 'admin' };
function isAdmin(value: User | Admin): value is Admin {
return 'role' in value;
}Guard custom untuk unknown
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}Type guard sangat penting saat data datang dari luar sistem: request body, local storage, API response, atau parser.
Kapan type guard dipakai
- saat mempersempit union yang kompleks,
- saat memvalidasi data dari boundary runtime,
- saat kamu ingin reuse pengecekan yang sama di banyak fungsi.
Jebakan type guards
- guard yang salah bikin compiler percaya pada hal yang tidak benar,
- truthy check tidak selalu aman untuk data yang valid bernilai falsy,
- assertion (
as) bukan pengganti pembuktian runtime.
Komposisi tipe untuk reuse
Bagian paling berguna dari advanced types adalah reuse yang muncul dari komposisi, bukan dari copy-paste tipe.
Pola reuse yang sehat
- Definisikan model inti sekali.
- Turunkan variasi dengan mapped types.
- Tambahkan branching dengan conditional types.
- Ekstrak bagian yang berulang dengan
infer. - Cocokkan hasilnya di runtime dengan type guards.
Contoh alur:
type User = {
id: string;
name: string;
email: string;
};
type UserPatch = Partial<User>;
type ApiResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string };
type UserResult = ApiResult<User>;Di sini:
Useradalah sumber kebenaran,Partial<User>memetakan kebutuhan update,ApiResult<T>dipakai ulang untuk banyak domain,- union dengan discriminator menjaga runtime flow tetap jelas.
Kenapa komposisi penting
- perubahan model inti otomatis merambat ke turunan yang relevan,
- tipe tidak tersebar ke banyak file sebagai salinan yang rawan drift,
- helper type menjadi aset jangka panjang, bukan sekadar workaround.
Jebakan umum saat type logic makin rumit
1. Terlalu banyak abstraksi sekaligus
Kalau satu tipe menggabungkan union, intersection, conditional, dan infer dalam satu baris panjang, pembaca biasanya berhenti memahami logikanya.
Solusi:
- pecah jadi alias kecil,
- beri nama yang menjelaskan fungsi,
- simpan tipe kompleks di file helper terpisah bila perlu.
2. Type-level logic mengalahkan desain domain
Kadang tipe jadi terlalu canggih hanya untuk menutupi model domain yang sebenarnya belum jelas.
Kalau struktur datanya masih berubah-ubah, jangan buru-buru optimalkan tipe. Rapikan model domain dulu.
3. never muncul tanpa sengaja
never sering berarti:
- pattern tidak cocok,
- union sudah tersaring habis,
- conditional type tidak menemukan cabang yang valid.
Kalau ini terjadi, cek asumsi input type dan pattern matching-nya.
4. Ketergantungan berlebihan pada assertion
as SomeType memang cepat, tapi kalau dipakai terus, kamu hanya memindahkan masalah ke runtime.
5. Tipe bagus, runtime tetap salah
TypeScript hanya membantu di compile time. Kalau data dari API salah, atau guard kamu lemah, runtime tetap bisa rusak.
Jadi, advanced types harus dipasangkan dengan:
- validasi runtime,
- schema check,
- dan boundary handling yang jelas.
Contoh ringkas pola yang sehat
type ApiSuccess<T> = { status: 'success'; data: T };
type ApiError = { status: 'error'; message: string };
type ApiResult<T> = ApiSuccess<T> | ApiError;
type ExtractData<T> = T extends ApiSuccess<infer R> ? R : never;
function isSuccess<T>(result: ApiResult<T>): result is ApiSuccess<T> {
return result.status === 'success';
}
function getData<T>(result: ApiResult<T>): T | null {
if (isSuccess(result)) {
return result.data;
}
return null;
}Pola ini menunjukkan hubungan yang rapi antara:
- union untuk status,
inferuntuk ekstraksi,- type guard untuk narrowing runtime.
Referensi singkat istilah
- Union:
A | B, satu dari beberapa kemungkinan. - Intersection:
A & B, gabungan semua kontrak. - Mapped type: transformasi properti dengan
keyofdan[K in ...]. - Conditional type: tipe yang bercabang berdasarkan kondisi.
infer: mekanisme ekstraksi tipe di dalam conditional type.- Type guard: fungsi atau ekspresi runtime yang membantu narrowing.
Referensi terkait
Types, Interfaces, Generics, Narrowing, dan Utility Types di TypeScript- dokumentasi resmi TypeScript: advanced types, conditional types, dan narrowing