Tipe di Rust sering tumbuh panjang: satu deklarasi seperti HashMap<u64, Vec<String>> sudah cukup melelahkan dibaca, dan tipe yang melibatkan trait object bisa lebih panjang lagi. Menuliskan tipe sepanjang itu berulang kali di signature fungsi, deklarasi variabel, dan nilai kembalian membuat kode sulit dipelihara — satu perubahan bentuk tipe harus dikejar ke banyak tempat. Type alias menyelesaikan kelelahan tersebut dengan satu kata kunci type: kita memberi nama ringkas untuk tipe panjang, dan nama itu dapat dipakai di seluruh kode. Penting dipahami sejak awal bahwa alias hanyalah nama lain — bukan tipe baru — sehingga perilakunya sangat berbeda dari newtype yang kita pelajari di Chapter sebelumnya.
Menyingkat Tipe yang Panjang #
Kata kunci type membuat alias dengan bentuk type Nama = Tipe;. Setelah deklarasi itu, Nama dapat menggantikan Tipe di posisi mana pun. Mari kita lihat pada kasus riwayat pesanan pelanggan:
use std::collections::HashMap;
type CustomerId = u64;
type OrderHistory = HashMap<CustomerId, Vec<String>>;
fn main() {
let mut history: OrderHistory = HashMap::new();
history.insert(101, vec![String::from("Kopi Gayo"), String::from("Teh Melati")]);
history.insert(202, vec![String::from("Roti Gandum")]);
let customer: CustomerId = 101;
match history.get(&customer) {
Some(orders) => {
println!("Pelanggan {} punya {} pesanan:", customer, orders.len());
for order in orders {
println!("- {}", order);
}
}
None => println!("Pelanggan {} belum pernah memesan", customer),
}
}Output yang Dihasilkan:
Pelanggan 101 punya 2 pesanan:
- Kopi Gayo
- Teh MelatiMari kita bedah peran setiap baris pentingnya:
- Baris 3 (
type CustomerId = u64;): aliasCustomerIdmenggantikanu64pada seluruh kode setelah baris ini. Pembaca kode kini tahu bahwau64tersebut adalah id pelanggan, bukan angka biasa. - Baris 4 (
type OrderHistory = HashMap<CustomerId, Vec<String>>;): satu nama mewakili tipe panjang. Alias juga boleh merujuk alias lain, sepertiCustomerIddi dalam definisi ini. - Baris 7 (
let mut history: OrderHistory = HashMap::new();): anotasi tipe menjadi pendek dan jelas. Tanpa alias, baris ini berbunyilet mut history: HashMap<u64, Vec<String>> = HashMap::new();. - Baris 9-10 (
history.insert(...)): pemanggilaninsertbekerja normal karenaOrderHistoryadalahHashMapyang sama — alias tidak mengubah perilaku tipe sedikit pun. - Baris 12 (
let customer: CustomerId = 101;): nilai101bertipeCustomerId, yang bagi kompilator identik denganu64. - Baris 14 (
match history.get(&customer) {):getmengambil&u64sebagai kunci; alias diterima tanpa konversi apa pun karenaCustomerIddanu64adalah tipe yang sama.
Alias adalah Nama Lain, Bukan Tipe Baru #
Di sinilah letak perbedaan mendasar dengan newtype. Alias tidak menciptakan tipe baru; kompilator hanya mengganti nama dengan tipe aslinya saat memeriksa kode. Dua alias yang berbeda nama tetapi merujuk tipe yang sama tetap dapat saling menggantikan. Mari kita buktikan dengan kasus satuan dari Chapter sebelumnya:
type Meters = u32;
type Millimeters = u32;
fn calculate_total_length(length_a: Meters, length_b: Meters) -> u32 {
length_a + length_b
}
fn main() {
let base_length: Meters = 3;
let precision: Millimeters = 500;
let total = calculate_total_length(base_length, precision);
println!("Total: {}", total);
}Output yang Dihasilkan:
Total: 503Program di atas dikompilasi tanpa error dan tanpa peringatan. Pada Chapter Newtype Pattern, program yang bentuknya sama tetapi memakai tuple struct ditolak keras oleh kompilator dengan E0308. Penjelasannya: setelah deklarasi type Meters = u32; dan type Millimeters = u32;, kedua nama itu adalah u32 yang sama di mata kompilator. Nilai precision bertipe Millimeters diterima parameter bertipe Meters tanpa diperiksa, dan hasil penjumlahan 3 meter + 500 (yang seharusnya milimeter) menghasilkan angka 503 yang salah makna. Alias membuat kode mudah dibaca manusia, tetapi alias tidak memberi perlindungan apa pun di sistem tipe.
Alias untuk Tipe Generik #
Kekuatan alias bertambah ketika aliasnya memuat parameter generik. Pola seperti ini sangat umum untuk menyembunyikan tipe error yang panjang di seluruh modul. Perhatikan contoh fungsi parsing usia berikut:
type ParseResult<T> = std::result::Result<T, String>;
fn parse_age(input: &str) -> ParseResult<u32> {
input
.trim()
.parse::<u32>()
.map_err(|_| format!("teks '{}' bukan angka usia yang valid", input))
}
fn main() {
match parse_age("29") {
Ok(age) => println!("Usia tercatat: {}", age),
Err(message) => println!("Error: {}", message),
}
match parse_age("tiga puluh") {
Ok(age) => println!("Usia tercatat: {}", age),
Err(message) => println!("Error: {}", message),
}
}Output yang Dihasilkan:
Usia tercatat: 29
Error: teks 'tiga puluh' bukan angka usia yang validMari kita bedah kode tersebut:
- Baris 1 (
type ParseResult<T> = std::result::Result<T, String>;): alias generik —ParseResult<u32>berartiResult<u32, String>,ParseResult<String>berartiResult<String, String>, dan seterusnya mengikuti parameter yang kita berikan. - Baris 3 (
fn parse_age(input: &str) -> ParseResult<u32> {): nilai kembalian terbaca sebagai “hasil parse u32” tanpa mengungkap detail tipe error. Signature singkat seperti ini mudah dipertahankan; jika kelak tipe error berubah menjadi enum kustom, kita cukup mengubah satu baris alias. - Baris 6 (
.parse::<u32>()): parsing teks menjadiu32menghasilkanResult<u32, ParseIntError>;map_errdi baris 7 mengganti error tersebut menjadiStringagar cocok dengan tipe pada alias. - Baris 11-18 (dua blok
match): kedua pemanggilan memakaiParseResult<u32>sebagai hasil dan menanganiOksertaErrsecara eksplisit.
Pola yang sama dipakai di standard library. Tipe std::io::Result<T> yang sering kita jumpai sebenarnya adalah alias untuk Result<T, std::io::Error>. Kode std memakai alias tersebut agar setiap signature berbisnis I/O tidak perlu menuliskan panjang lebar tipe errornya.
Alias untuk Trait Object yang Panjang #
Trait object yang dibungkus smart pointer menghasilkan tipe terpanjang di Rust. Alias membuatnya hidup kembali:
type Transform = Box<dyn Fn(i32) -> i32>;
fn apply_all(values: &[i32], transform: &Transform) -> Vec<i32> {
values.iter().map(|v| transform(*v)).collect()
}
fn main() {
let double: Transform = Box::new(|v| v * 2);
let scores = [70, 85, 90];
let doubled = apply_all(&scores, &double);
println!("Skor asli: {:?}", scores);
println!("Skor baru: {:?}", doubled);
}Output yang Dihasilkan:
Skor asli: [70, 85, 90]
Skor baru: [140, 170, 180]Dua baris yang perlu diperhatikan:
- Baris 1 (
type Transform = Box<dyn Fn(i32) -> i32>;): namaTransformkini mewakili closure pemilik di heap yang menerima dan menghasilkani32. Bayangkan menuliskan&Box<dyn Fn(i32) -> i32>pada parameterapply_alltanpa alias — signature fungsi baris 3 akan penuh sesak. - Baris 9 (
let double: Transform = Box::new(|v| v * 2);): closure dikemas ke dalamBoxdan diberi tipeTransform. Nilai tersebut lalu dipinjamkan sebagai&Transformke fungsiapply_alltanpa pemindahan kepemilikan.
Memilih Antara Alias dan Newtype #
Pengalaman di atas bisa dirangkum menjadi pedoman memilih:
- Pakai type alias ketika tujuannya murni keringkasan dan keterbacaan — misalnya menyembunyikan
HashMappanjang, tipeResultdengan error spesifik, atau trait object berbungkusBox. - Pakai newtype ketika kebenaran program bergantung pada pembedaan tipe — misalnya memisahkan satuan (
MetersvsMillimeters), id dari beberapa entitas berbeda, atau nilai yang harus melewati validasi. - Keduanya boleh dipakai bersama: alias untuk menyederhanakan newtype yang panjang, misalnya
type CustomerIds = Vec<UserId>;.
Ringkasan #
Type alias memberi nama ringkas untuk tipe berlength berapapun dengan satu deklarasi type Nama = Tipe;, termasuk alias generik seperti ParseResult<T> dan alias untuk trait object seperti Box<dyn Fn>. Alias tidak menciptakan tipe baru: Meters dan Millimeters yang keduanya beralias ke u32 dapat saling menggantikan tanpa diperiksa kompilator, sehingga perlindungan tipe harus tetap dipercayakan pada newtype. Standard library sendiri memakai pola ini melalui std::io::Result<T>. Setelah urusan penamaan tipe selesai, kita berpindah ke tipe yang bahkan tidak punya nilai sama sekali: never type ! pada Chapter berikutnya.