Kita telah menjelajahi kekuatan TypeScript Basic Utility Types dan Advanced Utility Types yang disediakan secara built-in. Mereka adalah alat yang luar biasa untuk memanipulasi dan mentransformasi tipe data yang sudah ada. Namun, bagaimana jika Anda menghadapi skenario yang sangat spesifik di mana utility types bawaan tidak cukup?
Di sinilah Custom TypeScript Utility Types berperan. Dengan memahami beberapa konsep inti TypeScript, Anda dapat membangun utility types Anda sendiri yang disesuaikan dengan kebutuhan proyek Anda. Ini akan memberi Anda kontrol tak terbatas atas sistem tipe Anda, memungkinkan Anda menciptakan abstraksi yang lebih tinggi dan keamanan tipe yang lebih presisi.
Mengapa Membuat Custom Utility Types?
Meskipun TypeScript menyediakan banyak utility types yang berguna, ada kalanya Anda membutuhkan logika transformasi tipe yang sangat spesifik yang tidak tercakup. Membuat custom utility types memungkinkan Anda untuk:
- Meningkatkan Reusability: Definisikan logika tipe yang kompleks sekali, lalu gunakan berulang kali.
- Meningkatkan Keterbacaan: Abstraksi logika tipe yang rumit menjadi nama tipe yang mudah dipahami.
- Mencapai Type Safety yang Lebih Tinggi: Terapkan aturan tipe yang sangat spesifik untuk mencegah kesalahan runtime.
- Mendukung Domain-Specific Logic: Ciptakan tipe yang sangat cocok dengan model data atau logika bisnis unik Anda.
Untuk membangun custom utility types, kita akan berfokus pada tiga konsep fundamental: Conditional Types, Mapped Types, dan Template Literal Types.
Konsep Fundamental untuk Custom Utility Types
1. Conditional Types (T extends U ? X : Y
)
Conditional Types
memungkinkan Anda memilih tipe yang berbeda berdasarkan kondisi. Sintaksnya mirip dengan operator ternary di JavaScript: T extends U ? X : Y
. Jika tipe T
dapat ditetapkan ke tipe U
, maka hasilnya adalah tipe X
; jika tidak, hasilnya adalah tipe Y
.
- Sintaks:
TypeA extends TypeB ? TypeC : TypeD
- Kapan Digunakan:
- Untuk membuat tipe yang bergantung pada properti atau bentuk tipe lain.
- Dalam kombinasi dengan
infer
untuk mengekstrak tipe dari bagian lain dari tipe yang sedang diperiksa.
Contoh:
// Contoh 1: Memilih tipe berdasarkan apakah itu string atau number
type IsString<T> = T extends string ? "Yes, it's a string" : "No, it's not a string";
// --- Penggunaan yang Benar ---
type Result1 = IsString<"hello">; // Hasil: "Yes, it's a string"
type Result2 = IsString<123>; // Hasil: "No, it's not a string"
type Result3 = IsString<boolean>; // Hasil: "No, it's not a string"
// Contoh 2: Mengekstrak tipe elemen array (menggunakan 'infer')
type ElementType<T> = T extends (infer U)[] ? U : T;
// --- Penggunaan yang Benar ---
type StringArrayElement = ElementType<string[]>; // Hasil: string
type NumberArrayElement = ElementType<number[]>; // Hasil: number
type NonArrayElement = ElementType<boolean>; // Hasil: boolean (karena bukan array)
// Contoh 3: Mengekstrak tipe properti dari objek jika properti tersebut adalah fungsi
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
interface Service {
id: number;
getName(): string;
isActive: boolean;
greet(message: string): void;
}
// --- Penggunaan yang Benar ---
type ServiceFunctionNames = FunctionPropertyNames<Service>; // Hasil: "getName" | "greet"
type ServiceFunctions = FunctionProperties<Service>;
/* Hasil:
{
getName: () => string;
greet: (message: string) => void;
}
*/
const myService: ServiceFunctions = {
getName: () => "My Service",
greet: (msg: string) => console.log(msg)
};
myService.greet("Hello from service!");
// --- Kesalahan Umum / Batasan ---
// Conditional Types bisa menjadi sangat kompleks dan sulit dibaca jika kondisi bersarang terlalu dalam.
// Hati-hati dengan 'infer' di tempat yang tidak tepat, bisa menghasilkan inferensi yang tidak diinginkan.
// Misalnya, `type WhatIsThis<T> = T extends infer U ? U : never;` akan selalu menghasilkan `T`.
2. Mapped Types ([P in K]: T
)
Mapped Types
memungkinkan Anda membuat tipe objek baru dengan mengiterasi melalui properti (kunci) dari tipe yang sudah ada dan menerapkan transformasi pada setiap properti. Mereka menggunakan sintaks [P in K]
, di mana P
adalah variabel untuk setiap kunci, dan K
adalah union type dari kunci-kunci yang akan diiterasi.
- Sintaks:
{ [P in K]: TypeTransformation }
- Kapan Digunakan:
- Untuk mengubah properti objek secara seragam (misalnya, membuat semua properti menjadi opsional, read-only, atau nullable).
- Membangun tipe baru berdasarkan properti tipe lain.
- Mengubah nama properti (Key Remapping).
Contoh:
interface UserData {
id: number;
name: string;
email: string;
}
// Contoh 1: Membuat semua properti menjadi nullable
type Nullable<T> = { [P in keyof T]: T[P] | null };
// --- Penggunaan yang Benar ---
type NullableUserData = Nullable<UserData>;
/* Hasil:
{
id: number | null;
name: string | null;
email: string | null;
}
*/
const userWithNulls: NullableUserData = {
id: 1,
name: "John Doe",
email: null // Email bisa null
};
console.log("User with Nulls:", userWithNulls);
// Contoh 2: Membuat semua properti menjadi fungsi getter (dengan Key Remapping)
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
};
// Catatan: 'as `get${Capitalize<string & P>}`' adalah fitur "Key Remapping" yang diperkenalkan di TS 4.1.
// `Capitalize<string & P>` memastikan P adalah string dan huruf pertamanya dikapitalisasi.
// --- Penggunaan yang Benar ---
type UserGetters = Getters<UserData>;
/* Hasil:
{
getId: () => number;
getName: () => string;
getEmail: () => string;
}
*/
const userGetters: UserGetters = {
getId: () => 1,
getName: () => "Alice",
getEmail: () => "[email protected]"
};
console.log("User Getter Name:", userGetters.getName());
// Contoh 3: Membuat semua properti menjadi Readonly secara rekursif (bersama Conditional Types)
// Ini adalah versi sederhana, DeepReadonly yang robust lebih kompleks
type DeepReadonly<T> = T extends object ? {
readonly [P in keyof T]: DeepReadonly<T[P]>;
} : T;
interface DeepObject {
a: number;
b: {
c: string;
d: {
e: boolean;
};
};
}
// --- Penggunaan yang Benar ---
type ImmutableDeepObject = DeepReadonly<DeepObject>;
const immutableObj: ImmutableDeepObject = {
a: 1,
b: {
c: "text",
d: {
e: true
}
}
};
// immutableObj.a = 2; // Error: Cannot assign to 'a' because it is a read-only property.
// immutableObj.b.c = "new text"; // Error: Cannot assign to 'c' because it is a read-only property.
// --- Kesalahan Umum / Batasan ---
// Mapped Types bekerja pada properti objek. Jika diterapkan pada tipe non-objek,
// hasilnya mungkin tidak seperti yang diharapkan atau menghasilkan tipe kosong.
// Hindari Key Remapping yang menghasilkan nama kunci yang sama, bisa menimbulkan konflik tipe.
3. Template Literal Types (${Prefix}${T}${Suffix}
)
Template Literal Types
memungkinkan Anda membuat string literal types baru dengan menggabungkan string literal types yang sudah ada dengan union types dan generic types. Mereka menggunakan sintaks template literal JavaScript (``
).
- Sintaks:
`${Prefix}${TypeVariable}${Suffix}`
- Kapan Digunakan:
- Untuk membuat string literal types yang dinamis, seperti nama event, kunci objek, atau path URL.
- Memastikan konsistensi penamaan dalam sistem yang besar.
Contoh:
type EventCategory = "user" | "product" | "order";
type EventAction = "created" | "updated" | "deleted";
// Contoh 1: Membuat tipe untuk nama event
type AppEvent = `${EventCategory}_${EventAction}`;
// --- Penggunaan yang Benar ---
const userCreatedEvent: AppEvent = "user_created";
const productUpdatedEvent: AppEvent = "product_updated";
// const orderViewedEvent: AppEvent = "order_viewed"; // Error: Type '"order_viewed"' is not assignable to type 'AppEvent'.
// Keterangan: Hanya kombinasi yang valid dari EventCategory dan EventAction yang diizinkan.
console.log("User Created Event:", userCreatedEvent);
// Contoh 2: Membuat tipe untuk kunci objek dengan prefix
type Feature = "settings" | "profile";
type FeatureConfigKeys = `config_${Feature}`;
// --- Penggunaan yang Benar ---
const settingKey: FeatureConfigKeys = "config_settings";
const profileKey: FeatureConfigKeys = "config_profile";
console.log("Setting Key:", settingKey);
// Contoh 3: Menggunakan infer untuk mengekstrak bagian dari string literal (reverse template literal)
type GetIdFromKey<T extends string> = T extends `${infer Prefix}_${infer Id}` ? Id : never;
// --- Penggunaan yang Benar ---
type ProductId = GetIdFromKey<"product_123">; // Hasil: "123"
type UserId = GetIdFromKey<"user_abc_456">; // Hasil: "abc_456" (infer mengambil sampai match pertama)
type NoId = GetIdFromKey<"just_a_string">; // Hasil: "a_string" (masih match pola)
type NoMatch = GetIdFromKey<"no_underscore">; // Hasil: never (jika pola tidak match)
console.log("Product ID Extracted:", "123" as ProductId);
// --- Kesalahan Umum / Batasan ---
// Template Literal Types hanya bekerja dengan string literal types.
// Jika salah satu bagian adalah tipe yang luas (seperti `number` atau `string` umum), hasilnya juga akan menjadi tipe yang luas.
// Misalnya, `type DynamicString = `${number}_${string}`;` akan menghasilkan `string`, bukan union literal spesifik.
Menggabungkan Custom Utility Types
Kekuatan sebenarnya dari custom utility types muncul ketika Anda menggabungkan konsep-konsep ini.
Contoh Kombinasi 1: Membuat Tipe DeepPartial
Partial
bawaan hanya membuat properti di level pertama menjadi opsional. Bagaimana jika kita ingin semua properti, termasuk properti di dalam objek bersarang, menjadi opsional?
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: DeepPartial<T[P]>;
} : T;
interface NestedUser {
id: number;
info: {
name: string;
contact?: {
email: string;
phone: string;
};
};
preferences: {
theme: "dark" | "light";
notifications: boolean;
};
}
// --- Penggunaan yang Benar ---
type PartialNestedUser = DeepPartial<NestedUser>;
/* Hasil:
{
id?: number | undefined;
info?: {
name?: string | undefined;
contact?: {
email?: string | undefined;
phone?: string | undefined;
} | undefined;
} | undefined;
preferences?: {
theme?: "dark" | "light" | undefined;
notifications?: boolean | undefined;
} | undefined;
}
*/
const partialUser: PartialNestedUser = {
info: {
contact: {
email: "[email protected]" // Hanya email yang disediakan, phone opsional
}
},
preferences: {
theme: "dark" // Hanya theme yang disediakan, notifications opsional
}
};
console.log("Partial Nested User:", partialUser);
// --- Kesalahan Umum / Batasan ---
// Recursive types seperti DeepPartial harus ditangani dengan hati-hati agar tidak menyebabkan infinite type evaluation.
// TypeScript memiliki batasan kedalaman rekursi (biasanya 50 level) yang bisa menyebabkan error jika struktur terlalu dalam.
Contoh Kombinasi 2: PropWithType<T, Type>
– Memilih Properti Berdasarkan Tipenya
Ini adalah utility type kustom yang memungkinkan Anda memilih hanya properti dari sebuah objek yang memiliki tipe tertentu.
type PropWithType<T, SelectedType> = {
[K in keyof T]: T[K] extends SelectedType ? K : never
}[keyof T];
interface Employee {
id: number;
name: string;
email: string;
hireDate: Date;
salary: number;
isActive: boolean;
}
// --- Penggunaan yang Benar ---
type StringProps = PropWithType<Employee, string>; // Hasil: "name" | "email"
type NumberProps = PropWithType<Employee, number>; // Hasil: "id" | "salary"
type BooleanProps = PropWithType<Employee, boolean>; // Hasil: "isActive"
type DateProps = PropWithType<Employee, Date>; // Hasil: "hireDate"
// Kemudian bisa digabungkan dengan Pick:
type EmployeeStringData = Pick<Employee, StringProps>;
/* Hasil:
{
name: string;
email: string;
}
*/
const employeeString: EmployeeStringData = {
name: "Agus",
email: "[email protected]"
};
console.log("Employee String Data:", employeeString);
// --- Kesalahan Umum / Batasan ---
// Hati-hati dengan tipe yang terlalu umum (misalnya, `any` atau `unknown`) sebagai `SelectedType`,
// karena bisa menghasilkan semua properti.
// Ini hanya mengecek tipe properti itu sendiri, bukan properti di dalam objek bersarang.
Membangun custom utility types adalah langkah terakhir dalam menguasai sistem tipe TypeScript. Dengan memahami dan menggabungkan Conditional Types
, Mapped Types
, dan Template Literal Types
, Anda memiliki kekuatan untuk membuat abstraksi tipe yang sangat canggih dan spesifik, sesuai dengan kebutuhan unik proyek Anda.
Ini memungkinkan Anda untuk menulis kode yang lebih aman, lebih maintainable, dan lebih ekspresif, mendorong batas-batas apa yang mungkin dilakukan dengan tipe data. Teruslah bereksperimen, dan Anda akan menemukan bahwa TypeScript adalah alat yang jauh lebih fleksibel dan kuat daripada yang Anda bayangkan.
Ini mengakhiri seri kita tentang TypeScript Utility Types. Selamat bereksperimen dan membangun sistem yang luar biasa dengan TypeScript!