Variabel #
Variabel adalah kontainer untuk menyimpan nilai — konsep yang sama di semua bahasa pemrograman. Tapi cara TypeScript menangani variabel berbeda secara fundamental dari JavaScript: TypeScript memperkenalkan tipe statis yang membuat setiap variabel memiliki “identitas tipe” yang dijaga compiler sepanjang hidupnya. Salah satu keputusan terpenting saat menulis TypeScript adalah memilih kata kunci deklarasi yang tepat (let, const, atau var) dan memahami kapan anotasi tipe perlu ditulis secara eksplisit versus kapan bisa diserahkan ke mekanisme type inference. Artikel ini membahas semua aspek variabel di TypeScript secara mendalam — termasuk beberapa jebakan yang sering tidak disadari developer yang baru beralih dari JavaScript.
Tiga Kata Kunci Deklarasi: let, const, var
#
TypeScript mewarisi ketiga kata kunci deklarasi dari JavaScript modern, tapi punya preferensi yang jelas: const adalah pilihan pertama, let untuk yang memang perlu berubah, dan var hampir tidak pernah digunakan lagi. Memahami mengapa demikian membutuhkan pemahaman tentang scoping — aturan tentang di mana sebuah variabel bisa diakses.
let — Block-Scoped, Bisa Diubah
#
let mendeklarasikan variabel yang cakupannya dibatasi oleh blok { } terdekat — baik itu blok fungsi, if, loop, atau blok kosong. Variabel let bisa diubah nilainya setelah deklarasi.
let usia: number = 25;
let nama: string = "Budi Santoso";
let aktif: boolean = true;
// Nilai bisa diubah — ini valid
usia = 26;
nama = "Budi";
// Tapi tipe tidak bisa berubah
// usia = "dua puluh enam"; // ✗ Error: Type 'string' is not assignable to type 'number'
Block scope berarti variabel let tidak “bocor” keluar dari blok tempat ia dideklarasikan:
function cekUsia(usia: number): void {
if (usia >= 18) {
let status = "dewasa"; // Hanya ada di dalam blok if ini
console.log(status); // ✓ "dewasa"
}
// console.log(status); // ✗ Error: Cannot find name 'status'
}
const — Block-Scoped, Tidak Bisa Di-reassign
#
const mendeklarasikan variabel yang tidak bisa di-reassign — kamu tidak bisa menunjuk const ke nilai baru setelah deklarasi awal. Ini bukan berarti nilainya sepenuhnya immutable, tapi binding-nya yang tidak bisa berubah.
const PI: number = 3.14159;
const NAMA_APLIKASI: string = "MuslimApps";
// Tidak bisa di-reassign
// PI = 3.14; // ✗ Error: Cannot assign to 'PI' because it is a constant
Fakta penting tentang const dan object: const hanya melindungi referensi variabel, bukan isi dari object atau array yang dirujuknya. Properti object dan elemen array masih bisa dimodifikasi:
const pengguna = {
nama: "Budi",
usia: 25,
};
// BENAR: Mengubah properti — tidak melanggar const
pengguna.usia = 26;
pengguna.nama = "Budi Santoso";
// ANTI-PATTERN: Mengira seluruh object immutable karena const
// const menjamin pengguna selalu menunjuk ke object yang sama,
// bukan bahwa isi object tidak bisa berubah
// ✗ Error: Ini yang tidak bisa dilakukan — re-assign ke object baru
// pengguna = { nama: "Siti", usia: 30 };
Hal yang sama berlaku untuk array:
const angka: number[] = [1, 2, 3];
angka.push(4); // ✓ Boleh — memodifikasi isi array
angka[0] = 99; // ✓ Boleh — mengubah elemen
// angka = [10, 20, 30]; // ✗ Error — re-assign ke array baru tidak boleh
Jika kamu benar-benar ingin object yang tidak bisa dimodifikasi sama sekali, gunakan Object.freeze() atau tipe Readonly<T>:
// Readonly<T> — semua properti menjadi readonly di level TypeScript
const pengaturan: Readonly<{ tema: string; bahasa: string }> = {
tema: "gelap",
bahasa: "id",
};
// pengaturan.tema = "terang"; // ✗ Error: Cannot assign to 'tema' because it is a read-only property
var — Function-Scoped, Harus Dihindari
#
var adalah kata kunci deklarasi lama dari JavaScript yang memiliki perilaku scoping yang berbeda — ia terikat pada fungsi, bukan blok. Ini menyebabkan bug yang halus dan sulit dilacak, terutama dalam loop dan closure.
// Demonstrasi masalah klasik var vs let dalam loop + closure
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 ← Semua menampilkan nilai akhir i, bukan nilai saat iterasi
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Output: 0, 1, 2 ← Setiap closure menangkap nilai j-nya sendiri
Mengapa ini terjadi? Karena var i hanya ada satu di memori — semua closure dalam loop merujuk ke variabel yang sama. Saat setTimeout dieksekusi, loop sudah selesai dan i bernilai 3. Sebaliknya, let j membuat variabel baru di setiap iterasi, sehingga setiap closure menangkap nilai yang berbeda.
// ANTI-PATTERN: Menggunakan var — perilaku hoisting dan function scope
// bisa menyebabkan bug yang sangat sulit dilacak
function contohHoisting() {
console.log(nilai); // undefined — bukan error! var di-hoist ke atas fungsi
var nilai = 42;
console.log(nilai); // 42
}
// BENAR: Menggunakan let — akses sebelum deklarasi selalu error
function contohTemporal() {
// console.log(nilai); // ✗ Error: Cannot access 'nilai' before initialization
let nilai = 42;
console.log(nilai); // 42
}
Type Inference vs Anotasi Eksplisit #
TypeScript bisa menyimpulkan tipe variabel secara otomatis dari nilai yang diberikan saat inisialisasi — mekanisme ini disebut type inference. Kamu tidak selalu perlu menulis anotasi tipe secara eksplisit.
// Anotasi eksplisit — kamu memberi tahu compiler tipenya
let usia: number = 25;
let nama: string = "Budi";
let aktif: boolean = true;
// Type inference — compiler menyimpulkan tipenya dari nilai awal
let usia2 = 25; // TypeScript tahu ini number
let nama2 = "Budi"; // TypeScript tahu ini string
let aktif2 = true; // TypeScript tahu ini boolean
Keduanya menghasilkan perilaku type checking yang identik — TypeScript akan menolak assignment tipe yang salah di kedua kasus. Jadi kapan harus menulis anotasi eksplisit?
// Situasi 1: Variabel dideklarasikan tanpa nilai awal
// Tanpa anotasi, TypeScript menyimpulkan tipe 'any' — berbahaya
let hasil; // Tipe: any — ❌ kehilangan type safety
let hasil2: number; // Tipe: number — ✓ terjaga
// Situasi 2: Nilai yang kompleks atau ambigu
// Type inference bekerja tapi anotasi membuat intent lebih jelas
const config: { host: string; port: number; ssl: boolean } = {
host: "localhost",
port: 5432,
ssl: false,
};
// Situasi 3: Return type fungsi — selalu eksplisit untuk fungsi publik
function hitungLuas(panjang: number, lebar: number): number {
return panjang * lebar;
}
Panduan praktis: andalkan type inference untuk variabel lokal yang langsung diinisialisasi. Tulis anotasi eksplisit untuk variabel yang dideklarasikan tanpa nilai, parameter fungsi, dan return type fungsi.
Tipe any dan Alternatifnya yang Lebih Aman
#
any adalah katup darurat TypeScript — ia menonaktifkan seluruh type checking untuk variabel tersebut. Variabel bertipe any bisa menerima nilai apapun dan bisa diperlakukan seolah memiliki metode apapun tanpa error kompilasi.
let nilaiAny: any = 10;
nilaiAny = "teks"; // ✓ Tidak ada error
nilaiAny = true; // ✓ Tidak ada error
nilaiAny = { a: 1 }; // ✓ Tidak ada error
// Bahayanya: compiler tidak bisa menangkap bug ini
nilaiAny.metodeTidakAda(); // Tidak ada error kompilasi, tapi akan crash di runtime!
nilaiAny.properti.yang.panjang.sekali; // Juga tidak ada error kompilasi
any pada dasarnya mengembalikanmu ke JavaScript biasa tanpa manfaat TypeScript. Ada dua alternatif yang jauh lebih aman:
unknown — Tipe Aman untuk Nilai yang Tidak Diketahui
#
unknown adalah pengganti any yang aman. Seperti any, ia menerima semua nilai. Tapi berbeda dari any, kamu tidak bisa melakukan operasi apapun pada nilai unknown sebelum menyempitkan tipenya terlebih dahulu:
// ANTI-PATTERN: any — tidak ada perlindungan sama sekali
function prosesAny(nilai: any): string {
return nilai.toUpperCase(); // Tidak ada error kompilasi, tapi crash jika nilai bukan string
}
// BENAR: unknown — paksa narrowing sebelum operasi
function prosesUnknown(nilai: unknown): string {
if (typeof nilai === "string") {
return nilai.toUpperCase(); // ✓ Aman — TypeScript tahu ini string di sini
}
if (typeof nilai === "number") {
return nilai.toString(); // ✓ Aman — TypeScript tahu ini number di sini
}
return String(nilai); // Fallback yang aman
}
unknown sangat berguna untuk nilai yang datang dari luar sistem — response API, input pengguna, atau data dari JSON parsing:
async function ambilData(url: string): Promise<unknown> {
const response = await fetch(url);
return response.json(); // json() mengembalikan Promise<any>, kita wrap ke unknown
}
// Pengguna dipaksa melakukan validasi sebelum bisa menggunakan data
const data = await ambilData("https://api.example.com/pengguna");
// data.nama // ✗ Error: Object is of type 'unknown'
if (typeof data === "object" && data !== null && "nama" in data) {
console.log((data as { nama: string }).nama); // ✓ Setelah validasi
}
Perbandingan any vs unknown vs Tipe Spesifik
#
| Aspek | any | unknown | Tipe Spesifik |
|---|---|---|---|
| Menerima semua nilai | ✓ | ✓ | ✗ (hanya tipe itu) |
| Operasi tanpa validasi | ✓ | ✗ | ✓ |
| Type safety | ✗ Tidak ada | ✓ Setelah narrowing | ✓ Penuh |
| Cocok untuk | Legacy code, darurat | Data eksternal/dinamis | Mayoritas kasus |
Null dan Undefined: Penanganan yang Benar #
null dan undefined adalah dua sumber bug terbesar di JavaScript — dan TypeScript dirancang untuk menanganinya dengan baik melalui opsi strictNullChecks. Ketika opsi ini aktif (diaktifkan oleh strict: true), null dan undefined bukan lagi anggota dari setiap tipe secara otomatis.
// Dengan strictNullChecks: true (direkomendasikan)
let nama: string = "Budi";
// nama = null; // ✗ Error: Type 'null' is not assignable to type 'string'
// nama = undefined; // ✗ Error: Type 'undefined' is not assignable to type 'string'
// Untuk mengizinkan null/undefined, deklarasikan secara eksplisit
let namaNullable: string | null = null;
namaNullable = "Budi"; // ✓
namaNullable = null; // ✓
let namaOpsional: string | undefined = undefined;
namaOpsional = "Siti"; // ✓
namaOpsional = undefined; // ✓
Narrowing untuk Null Check #
Sebelum menggunakan variabel yang mungkin null atau undefined, kamu harus melakukan narrowing:
function sapaPengguna(nama: string | null): string {
// ANTI-PATTERN: Langsung gunakan tanpa cek — bisa crash
// return nama.toUpperCase(); // ✗ Error: Object is possibly 'null'
// BENAR: Narrowing terlebih dahulu
if (nama === null) {
return "Halo, Tamu!";
}
return `Halo, ${nama.toUpperCase()}!`;
}
// Atau gunakan optional chaining dan nullish coalescing
function sapaPendek(nama: string | null | undefined): string {
return `Halo, ${nama?.toUpperCase() ?? "Tamu"}!`;
}
Non-null Assertion Operator (!)
#
TypeScript menyediakan operator ! untuk memberitahu compiler bahwa kamu yakin sebuah nilai tidak null atau undefined — meski tipenya mengizinkan:
// Gunakan ! hanya jika kamu BENAR-BENAR yakin nilai tidak null/undefined
function ambilElemen(id: string): HTMLElement {
const elemen = document.getElementById(id);
// elemen bisa null jika ID tidak ditemukan
return elemen!; // Memberitahu compiler "percaya padaku, ini tidak null"
}
// ANTI-PATTERN: Menyalahgunakan ! untuk menghindari penanganan null yang proper
const elemen = document.getElementById("tombol")!;
elemen.click(); // Crash di runtime jika elemen tidak ada di DOM
Operator!(non-null assertion) adalah tanda bahaya yang sama dengan@ts-ignore— keduanya menonaktifkan pemeriksaan compiler secara lokal. Gunakan hanya jika kamu punya jaminan eksternal bahwa nilai tidak null (misalnya, test setup yang sudah memastikan DOM element ada). Jangan gunakan sebagai jalan pintas untuk menghindari null handling yang proper.
Destructuring dengan Tipe #
Destructuring adalah fitur JavaScript modern yang sangat umum digunakan — dan TypeScript menambahkan type inference yang cerdas di atasnya.
Destructuring Object #
interface Pengguna {
id: number;
nama: string;
email: string;
usia?: number;
}
const pengguna: Pengguna = {
id: 1,
nama: "Budi Santoso",
email: "[email protected]",
};
// Destructuring — TypeScript menyimpulkan tipe setiap variabel
const { id, nama, email, usia } = pengguna;
// id: number, nama: string, email: string, usia: number | undefined
// Rename saat destructuring
const { nama: namaPengguna, email: emailPengguna } = pengguna;
console.log(namaPengguna); // "Budi Santoso"
// Nilai default untuk properti opsional
const { usia: umur = 0 } = pengguna;
console.log(umur); // 0 — karena usia tidak ada di object
Destructuring Array dan Tuple #
const koordinat: [number, number] = [106.827, -6.175]; // [longitude, latitude] Jakarta
const [longitude, latitude] = koordinat;
console.log(`Lon: ${longitude}, Lat: ${latitude}`);
// Skip elemen dengan koma
const angka = [1, 2, 3, 4, 5];
const [pertama, , ketiga, ...sisanya] = angka;
// pertama: 1, ketiga: 3, sisanya: [4, 5]
Destructuring di Parameter Fungsi #
// Tanpa destructuring — verbose
function tampilkanPengguna(pengguna: Pengguna): string {
return `${pengguna.nama} <${pengguna.email}>`;
}
// Dengan destructuring di parameter — lebih bersih
function tampilkanPengguna2({ nama, email }: Pengguna): string {
return `${nama} <${email}>`;
}
// Dengan nilai default di parameter
function buatProfil({ nama, usia = 0 }: { nama: string; usia?: number }): string {
return `${nama} (${usia} tahun)`;
}
Scoping dan Hoisting: Diagram Visual #
Memahami perbedaan scoping antara var, let, dan const secara visual:
flowchart TD
A["Deklarasi Variabel"] --> B{"Kata Kunci?"}
B -- "var" --> C["Function Scope"]
B -- "let" --> D["Block Scope"]
B -- "const" --> E["Block Scope dan No Reassign"]
C --> C1["Bisa diakses di seluruh fungsi"]
C --> C2["Di-hoist ke atas fungsi<br/>nilai: undefined"]
C --> C3["Bisa di-redeclare<br/>dalam scope yang sama"]
D --> D1["Hanya dalam blok terdekat"]
D --> D2["Temporal Dead Zone<br/>sebelum deklarasi menjadi Error"]
D --> D3["Tidak bisa di-redeclare<br/>dalam scope yang sama"]
E --> E1["Hanya dalam blok terdekat"]
E --> E2["Temporal Dead Zone<br/>sebelum deklarasi menjadi Error"]
E --> E3["Binding tidak bisa diubah<br/>isi object atau array bisa berubah"]
style C fill:#ff6b6b,color:#fff
style C1 fill:#ffcccc
style C2 fill:#ffcccc
style C3 fill:#ffcccc
style D fill:#339af0,color:#fff
style E fill:#51cf66,color:#fffKonvensi Penamaan Variabel #
TypeScript mengikuti konvensi penamaan JavaScript yang sudah mapan. Konsistensi penamaan bukan aturan compiler — tapi penting untuk keterbacaan kode tim:
// camelCase — untuk variabel dan fungsi (paling umum)
let namaLengkap: string = "Budi Santoso";
let jumlahItem: number = 0;
const ambilData = () => {};
// PascalCase — untuk class, interface, type alias, dan enum
class AuthService {}
interface DataPengguna {}
type HasilOperasi = "sukses" | "gagal";
enum StatusPesanan { Aktif, NonAktif }
// SCREAMING_SNAKE_CASE — untuk konstanta global yang benar-benar tidak berubah
const BATAS_PERCOBAAN_LOGIN: number = 3;
const URL_API_PRODUKSI: string = "https://api.muslimapps.id";
const VERSI_APLIKASI: string = "2.1.0";
// Prefix untuk variabel boolean — gunakan kata kerja is/has/can/should
let isLoading: boolean = false;
let hasError: boolean = false;
let canEdit: boolean = true;
let shouldRefetch: boolean = false;
// ANTI-PATTERN: Nama yang tidak deskriptif
let d: number; // ✗ Apa ini? tanggal? durasi? diskon?
let tmp: string; // ✗ Temporary apa?
let data2: unknown; // ✗ Mengapa ada data dan data2?
// BENAR: Nama yang self-documenting
let tanggalDibuat: Date;
let pesanSementara: string;
let dataResponseAPI: unknown;
Ringkasan #
- Gunakan
constsecara default — switch kelethanya jika variabel memang perlu di-reassign; hindarivarsepenuhnya karena function scope dan hoisting-nya menyebabkan bug yang sulit dilacak.consttidak berarti immutable — ia hanya melindungi binding (referensi), bukan isi object atau array; gunakanReadonly<T>jika kamu butuh proteksi di level properti.- Type inference cukup untuk variabel lokal yang langsung diinisialisasi; tulis anotasi eksplisit untuk variabel tanpa nilai awal, parameter fungsi, dan return type fungsi.
- Hindari
any— gunakanunknownsebagai gantinya untuk nilai yang tipenya benar-benar tidak diketahui;unknownmemaksamu melakukan narrowing sebelum operasi, jauh lebih aman.strictNullChecksharus aktif — dengan opsi ini,nulldanundefinedtidak bisa diam-diam masuk ke variabel bertipe lain; kamu dipaksa menanganinya secara eksplisit.- Operator
!adalah tanda bahaya — gunakan non-null assertion hanya jika kamu punya jaminan eksternal bahwa nilai tidak null, bukan sebagai jalan pintas menghindari null handling.- Destructuring dengan type inference — TypeScript menyimpulkan tipe setiap variabel hasil destructuring secara otomatis; manfaatkan nilai default dalam destructuring untuk properti opsional.
- Konvensi penamaan: camelCase untuk variabel dan fungsi, PascalCase untuk type/interface/class/enum, SCREAMING_SNAKE_CASE untuk konstanta global, dan prefix
is/has/can/shoulduntuk variabel boolean.