Typescript Types Interfaces Generics Narrowing Utility Types
Definisi singkat
TypeScript menambahkan sistem tipe di atas JavaScript supaya struktur data, kontrak fungsi, dan alur kontrol lebih aman saat dikembangkan. Lima konsep yang paling sering dipakai sehari-hari adalah:
- type: alias untuk tipe apa pun, termasuk union, intersection, primitive, tuple, dan mapped type.
- interface: bentuk kontrak objek yang bisa di-extend dan di-merge.
- generics: parameter tipe untuk membuat fungsi, class, dan tipe yang bisa dipakai ulang.
- narrowing: proses mempersempit tipe berdasarkan pengecekan runtime.
- utility types: tipe bawaan TypeScript untuk transformasi tipe yang umum.
Kapan pakai type vs interface
Keduanya sering tumpang tindih, terutama untuk bentuk object. Pilih berdasarkan kebutuhan, bukan kebiasaan.
Pakai interface kalau
- kamu mendefinisikan bentuk object/domain model
- kamu ingin extend kontrak secara eksplisit
- kamu butuh declaration merging di beberapa tempat
- kamu sedang mendesain API publik yang cocok untuk di-augment
Contoh:
interface User {
id: string;
name: string;
}
interface Admin extends User {
role: 'admin';
}Pakai type kalau
- kamu butuh union atau intersection
- kamu ingin membuat alias untuk primitive, tuple, function type, atau kombinasi kompleks
- kamu ingin tipe turunan yang lebih fleksibel untuk transformasi
Contoh:
type Status = 'idle' | 'loading' | 'success' | 'error';
type Id = string | number;
type Point = [number, number];
type Handler = (value: string) => void;Aturan praktis
- Kalau bentuknya objek stabil,
interfacebiasanya lebih enak dibaca. - Kalau bentuknya komposisi tipe,
typebiasanya lebih kuat. - Kalau kamu ragu, pilih yang paling mudah dipahami tim; TypeScript tidak mewajibkan satu jawaban mutlak.
Perbedaan penting yang sering terasa di real project
interfacemendukungextendslangsung dan declaration merging.typelebih cocok untuk union/intersection dan transformasi tipe.typedaninterfacesama-sama bisa dipakai untuk object shape, tapitypetidak bisa dideclaration-merge sepertiinterface.
Generics untuk reuse
Generics dipakai saat kamu ingin menulis satu pola yang tetap aman untuk banyak jenis data.
Kenapa generics penting
Tanpa generics, kode reusable sering jatuh ke any atau union yang terlalu lebar. Dengan generics, TypeScript tetap bisa menjaga hubungan antara input dan output.
Contoh paling dasar
function identity<T>(value: T): T {
return value;
}
const a = identity('hello'); // string
const b = identity(123); // numberT di sini adalah parameter tipe. TypeScript mengisi nilainya dari argumen yang dipakai.
Contoh reuse pada helper data
function first<T>(items: T[]): T | undefined {
return items[0];
}
const user = first([{ id: 1 }, { id: 2 }]);Kelebihannya bukan cuma menghindari duplikasi. Yang lebih penting: tipe hasil tetap nyambung dengan tipe input.
Constraint dengan extends
Kadang generic harus dibatasi supaya properti tertentu pasti tersedia.
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength('abc');
getLength([1, 2, 3]);Ini berguna saat fungsi butuh bentuk minimal, tapi tetap ingin fleksibel.
Default generic dan generic pada type alias
type ApiResponse<T = unknown> = {
data: T;
error?: string;
};
type UserResponse = ApiResponse<{ id: string; name: string }>;Pola ini berguna untuk response API, wrapper state, dan komponen yang punya data shape berbeda-beda.
Jebakan generics
- terlalu banyak parameter generic membuat kode sulit dibaca
- generic tanpa constraint sering mendorong logika ke
anyatau type assertion - kalau hubungan input-output tidak penting, generic justru overkill
Rule of thumb: pakai generics saat tipe output bergantung pada input atau saat kamu ingin menulis abstraksi yang memang dipakai berulang.
Type narrowing
Narrowing adalah cara TypeScript mempersempit tipe dari bentuk yang lebih luas menjadi bentuk yang lebih spesifik berdasarkan pengecekan runtime.
Bentuk narrowing yang umum
1. typeof
function format(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}2. in
type A = { kind: 'a'; foo: string };
type B = { kind: 'b'; bar: number };
function handle(input: A | B) {
if ('foo' in input) {
return input.foo;
}
return input.bar;
}3. equality check
function parse(value: 'yes' | 'no' | boolean) {
if (value === true) {
return 'truthy';
}
return value;
}4. discriminated union
type Loading = { status: 'loading' };
type Success = { status: 'success'; data: string };
type ErrorState = { status: 'error'; message: string };
type State = Loading | Success | ErrorState;
function render(state: State) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return state.data;
case 'error':
return state.message;
}
}Discriminated union adalah pola paling aman dan paling enak dirawat untuk state yang punya beberapa mode.
Jebakan umum pada narrowing
1. Mengandalkan cek yang tidak cukup spesifik
function f(value: string | null) {
if (value) {
// string, tapi empty string ikut dianggap false
return value;
}
}Truthy check memang sering berguna, tapi untuk data yang valid bisa bernilai '', 0, atau false, check ini bisa salah arah. Lebih aman pakai pengecekan eksplisit kalau nilai kosong masih valid.
2. Narrowing hilang setelah mutasi
Kalau variabel berubah setelah dicek, asumsi tipe bisa jadi tidak valid lagi.
let value: string | number = 'abc';
if (typeof value === 'string') {
value = 123;
// di sini value sudah bukan string lagi
}3. Mengira narrowing runtime otomatis berlaku di semua tempat
TypeScript hanya memahami alur yang bisa dianalisis statis. Setelah data lewat fungsi lain, disimpan ke struktur yang lebih longgar, atau diubah jadi unknown/any, informasi tipe bisa hilang.
4. Type assertion dipakai untuk menutup error
const el = document.getElementById('app') as HTMLDivElement;Assertion bukan narrowing. Ini bilang ke compiler “percaya saja”, bukan membuktikan tipe. Kalau salah, bug pindah ke runtime.
5. Array.filter(Boolean) sering dianggap aman
Pola ini sering membuang nilai falsy yang sebenarnya valid, dan hasil tipenya juga tidak selalu sespesifik yang diharapkan. Kalau butuh filter tipe, pakai type guard eksplisit.
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const values: unknown[] = ['a', 1, 'b'];
const strings = values.filter(isString); // string[]Type guard yang rapi
Type guard dipakai saat logika narrowing berulang dan ingin dibungkus dalam fungsi.
function isSuccess(state: State): state is Success {
return state.status === 'success';
}Ini membantu saat pengecekan dipakai di beberapa tempat tanpa mengulang logika switch.
Utility types
Utility types adalah tipe bawaan TypeScript yang sangat membantu saat kamu ingin memodifikasi bentuk tipe tanpa menulis ulang dari nol.
Utility types yang paling sering dipakai
Partial<T>
Membuat semua properti jadi opsional.
type User = {
id: string;
name: string;
email: string;
};
type UserPatch = Partial<User>;Cocok untuk update form atau patch payload.
Required<T>
Membuat semua properti wajib.
Pick<T, K>
Mengambil properti tertentu saja.
type UserSummary = Pick<User, 'id' | 'name'>;Omit<T, K>
Membuang properti tertentu.
type PublicUser = Omit<User, 'email'>;Record<K, T>
Membuat object dengan key tertentu dan value seragam.
type Role = 'admin' | 'editor' | 'viewer';
type PermissionMap = Record<Role, boolean>;Readonly<T>
Membuat properti tidak bisa diubah.
ReturnType<T> dan Parameters<T>
Berguna saat ingin mengambil tipe dari fungsi yang sudah ada.
function buildUser(name: string) {
return { id: '1', name };
}
type BuiltUser = ReturnType<typeof buildUser>;
type BuildUserArgs = Parameters<typeof buildUser>;Kapan utility types dipakai
- saat membuat varian tipe dari model yang sudah ada
- saat ingin payload API yang berbeda dari entity utama
- saat membuat form state, patch request, atau response summary
Jebakan utility types
Partial<T>terlalu sering dipakai untuk semua kasus update, padahal bisa menyembunyikan field wajibOmit<T, K>tidak selalu aman kalau field yang dihapus sebenarnya masih dipakai di layer lain- utility types tidak menggantikan desain model yang rapi; mereka hanya membantu transformasi
Contoh pola yang sering dipakai
Model domain + payload update
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'member';
}
type UpdateUserPayload = Partial<Pick<User, 'name' | 'email' | 'role'>>;API response generik
type ApiResult<T> = {
data: T | null;
error: string | null;
};
type UserListResult = ApiResult<User[]>;State dengan discriminated union
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };Pola seperti ini biasanya lebih aman daripada satu object besar dengan banyak properti opsional.
Kesalahan umum
- memakai
anysaat sebenarnya cukupunknown+ narrowing - memakai assertion untuk memaksa compiler diam
- membangun union terlalu besar tanpa discriminant
- memakai
Partialdi semua tempat update - menulis generic tanpa manfaat nyata
- menganggap
interfaceselalu lebih baik daritype, atau sebaliknya
Ringkasan cepat
- Pakai interface untuk kontrak object yang stabil dan bisa di-extend.
- Pakai type untuk union, intersection, alias kompleks, dan transformasi tipe.
- Pakai generics saat ingin reuse tanpa kehilangan hubungan tipe input-output.
- Pakai narrowing untuk mengubah union menjadi tipe spesifik berdasarkan cek runtime.
- Pakai utility types untuk transformasi tipe yang umum, tapi jangan biarkan mereka menggantikan desain model.
Referensi terkait
- Dokumentasi TypeScript resmi
- Artikel ini paling nyambung dengan topik model data, API response, dan state management yang butuh tipe aman