Typescript Module Augmentation Declaration Merging Decorators
Definisi
Tiga mekanisme ini dipakai untuk memperluas atau menyusun ulang tipe tanpa menyentuh kode aslinya:
- Declaration Merging: TypeScript menyatukan beberapa deklarasi dengan nama sama dalam satu scope menjadi satu deklarasi final.
- Module Augmentation: menambah deklarasi (interface, class, fungsi, dll.) ke dalam module yang sudah ada lewat
declare module. - Decorators: ekspresi
@namayang menempel di class, method, property, accessor, atau parameter, lalu dieksekusi saat deklarasi tersebut dievaluasi — dipakai untuk menyuntikkan metadata atau memodifikasi perilaku.
Ketiganya beririsan di satu tema: TypeScript fleksibel karena tipenya bisa digabung dan diperluas secara deklaratif, bukan cuma ditulis ulang.
Kapan dipakai
- Declaration merging dipakai saat kamu punya beberapa potongan tipe dengan nama sama dan ingin TypeScript menggabungkannya otomatis (misal
interfaceyang didefinisikan di dua file berbeda, atau memperluas definisi bawaan). - Module augmentation dipakai saat kamu ingin menambah properti/method ke tipe milik package pihak ketiga atau ke tipe bawaan seperti
Window, tanpa fork kodenya. - Decorators dipakai saat kamu ingin menandai class/member dengan metadata (ORM, validasi, routing, DI) atau memodifikasi descriptor secara terpusat.
Cara kerja
Declaration merging
Yang bisa di-merge: interface, namespace, enum, dan kombinasi class + namespace. Aturan inti: deklarasi dengan nama sama digabung; kalau ada member dengan key sama, tipenya harus kompatibel (interface di-merge, bukan ditimpa).
// File A
interface User {
id: string;
name: string;
}
// File B (nama sama, di-merge)
interface User {
email: string;
}
// Hasil akhir: User punya id, name, dan email
const u: User = { id: '1', name: 'A', email: 'a@x.io' };Function overloads tidak di-merge lewat interface, tetapi lewat deklarasi overload berturut-turut:
function format(value: number): string;
function format(value: Date): string;
function format(value: number | Date): string {
return value.toString();
}Class bisa di-merge dengan namespace untuk menambah member statis, tapi tidak bisa di-merge dengan interface secara langsung:
class Counter {
count = 0;
}
namespace Counter {
export const reset = () => 0; // menempel sebagai Counter.reset
}Module augmentation
Bentuk paling umum: memperluas tipe di dalam sebuah module. Syarat: file augmentation harus punya setidaknya satu import/export agar dianggap sebagai module, bukan script global.
// menambah field ke tipe milik package pihak ketiga
import 'some-lib';
declare module 'some-lib' {
interface ClientOptions {
retryCount?: number;
}
}Untuk tipe global bawaan (seperti Window, Array, process), pakai declare global:
declare global {
interface Window {
analyticsId: string;
}
}
export {}; // agar file ini dianggap moduleDecorators
Decorator adalah function yang dipanggil saat deklarasi dievaluasi. Ada dua mode di TypeScript:
- Legacy (
experimentalDecorators: true): signature(target, propertyKey?, descriptor?). Metadata design-time butuhemitDecoratorMetadata: true+reflect-metadata. - TC39 standard (TypeScript 5.0+): pakai tipe
Decoratordan semantik baru. Untuk metadata masih mengandalkan jalur legacy.
import 'reflect-metadata';
function Entity(table: string) {
return (target: Function) => {
Reflect.defineMetadata('table', table, target);
};
}
@Entity('users')
class User {}
Reflect.getMetadata('table', User); // 'users'Decorator property untuk mengumpulkan metadata validasi:
function Required(target: object, propertyKey: string) {
const existing: string[] = Reflect.getMetadata('required', target) ?? [];
Reflect.defineMetadata('required', [...existing, propertyKey], target);
}
class SignupDTO {
@Required email: string;
@Required password: string;
}
Reflect.getMetadata('required', SignupDTO.prototype); // ['email', 'password']Augmentation vs declare module: kapan mana
Ini sumber kebingungan paling sering. Perbedaannya bukan di keyword, tapi di konteks file dan tujuannya.
Ringkasnya:
declare module 'x' { ... }di dalam file module (adaimport/export) → augmentation: memperluas modulexyang sudah punya tipe.declare module 'x' { ... }di dalam file global (tanpaimport/export) → ambient module declaration: memberi tipe pada module yang sama sekali belum punya definisi.d.ts.declare global { ... }→ memperluas tipe global bawaan (Window,Array, dll.).
Jadi declare module dipakai untuk dua hal berbeda tergantung ada/tidaknya import/export di file. Augmentation selalu butuh file module.
Catatan implementasi
- File augmentation wajib punya
import/export, kalau tidak TypeScript menganggapnya script global dan malah membuat ambient declaration baru, bukan memperluas yang ada. - Untuk augment module dengan
export =(CommonJS), pakaideclare module 'x' { import x = require('x'); ... }sesuai bentuk modulenya. - Decorator legacy butuh
experimentalDecorators: trueditsconfig. Tanpa itu,@namaakan error. - Metadata butuh
emitDecoratorMetadata: truedanreflect-metadatadi-import sekali di entry point. - Urutan evaluasi decorator: dari bawah ke atas untuk property/parameter dalam satu class, dan outer-to-inner untuk decorator bertingkat (
@a @b→bdieksekusi duluan).
Kesalahan umum saat override tipe bawaan
- Menimpa signature yang sudah ada.
declare global { interface Array<T> { push(x: T): void } }untuk mengubahpushakan errorsubsequent property declarations must have the same type. Aturan lib bawaan: hanya boleh menambah member, tidak mengganti atau menghapus. - Salah pakai
declare modulevsdeclare global. MemperluasWindowdengandeclare module 'window'tidak akan berlaku; pakaideclare global { interface Window {...} }. - Augmentasi global bentrok antar file. Dua
declare global { interface Array<T> {...} }dengan member key sama tapi tipe beda akan memunculkanDuplicate identifieratau error merging. - Lupa
export {}. File yang cuma berisideclare globaltanpaexportdianggap script global dan bisa mendeklarasikan ulang secara tidak sengaja. Tambahkanexport {}. - Augmentasi package salah path.
declare module 'react'memperluas React asli; kalau nama module salah ketik, TypeScript membuat module kosong baru alih-alih merge, sehingga tipe asli "hilang" di mata augmentation tersebut. - Mixin lewat class merging yang tidak valid.
class A {}tidak bisa di-merge denganinterface A {}. Gunakannamespaceuntuk menempelkan member statis ke class. - Metadata kosong. Lupa
emitDecoratorMetadataatau belumimport 'reflect-metadata'→Reflect.getMetadataselaluundefined. - Mencampur decorator legacy dan TC39 tanpa memilih satu mode di
tsconfig, menghasilkan signature decorator yang tidak cocok.
Referensi terkait
- Types, Interfaces, Generics, Narrowing, dan Utility Types di TypeScript
- Advanced Types TypeScript: Unions, Intersections, Mapped Types, Conditional Types
- tsconfig, Module Resolution, Path Aliases, Declaration Files, dan Build Targets
- Narrowing Patterns, Discriminated Unions, Exhaustive Checks, dan Runtime Validation Basics