TypeScript Generics dengan Claude Code: keyof, constraint, dan tipe API
Pelajari TypeScript Generics dengan Claude Code: constraint, keyof, mapped types, tipe API, dan verifikasi tsc.
Jangan hanya meminta “buat generic”
TypeScript Generics membuat satu function, type, atau class bisa dipakai untuk banyak bentuk data tanpa kehilangan hubungan antara input dan output. Kesulitan pemula biasanya bukan huruf T, tetapi prompt yang terlalu umum kepada Claude Code: “buat ini generic.” Tanpa menjelaskan bagian mana yang boleh fleksibel dan bagian mana yang harus dibatasi, hasilnya sering terlihat reusable tetapi diam-diam memakai any, unknown, atau Record<string, unknown> yang terlalu lebar.
Di proyek nyata, generics sering berada dekat dengan respons API, form, akun, billing, analytics event, dan CTA produk. Jika tipe di area itu lemah, masalahnya bukan sekadar autocomplete. Nilai yang salah bisa masuk ke form konsultasi, checkout, atau event konversi. Aturan praktis Masa: minta Claude Code membuat contoh yang valid dan contoh yang memang harus gagal saat tsc dijalankan.
Alur berpikirnya seperti ini:
nilai input -> ditangkap sebagai T -> key dibatasi dengan keyof T -> bentuk diubah dengan mapped types -> kontrak diverifikasi dengan tsc
Sintaks dalam artikel ini dicek dari dokumentasi resmi TypeScript: Generics, keyof Type Operator, Mapped Types, dan Conditional Types. Untuk workflow Claude Code yang lebih luas, lanjutkan ke tips TypeScript dengan Claude Code dan utility types dengan Claude Code.
Beri kontrak review sebelum kode
Generics adalah alat compile-time. T bukan variable runtime, melainkan type parameter yang membuat compiler mengingat tipe apa yang masuk dan tipe apa yang harus keluar. Dalam konteks ini, extends berarti “terima hanya tipe yang memenuhi bentuk ini.” keyof T membuat kumpulan nama property dari T. Mapped types berjalan di atas nama property itu untuk membangun tipe baru.
Sebelum Claude Code mengubah repository, beri tabel kontrak seperti ini:
| Pertanyaan | Yang diberikan ke Claude Code | Yang direview |
|---|---|---|
Apa arti T? | Domain object, DTO, atau form model | Result tidak kehilangan tipe asli |
| Apa yang harus dibatasi? | K extends keyof T, E extends ApiError, T extends object | Panggilan invalid gagal saat compile |
| Bagaimana verifikasinya? | @ts-expect-error, Expect, command tsc strict | Contoh buruk benar-benar gagal |
Tabel ini mencegah kegagalan umum: Claude Code menulis kode yang compile hanya karena ada cast di akhir. Cast bisa diterima saat mendokumentasikan transformasi yang tidak bisa diinfer TypeScript, tetapi tidak boleh menyembunyikan desain tipe yang terlalu longgar.
Use case 1: deduplikasi dengan key yang aman
Use case pertama adalah uniqueBy, berguna untuk baris API, import CSV, tabel admin, dan list UI. Jika key hanya bertipe string, caller bisa mengirim property yang tidak ada. Dengan K extends keyof T, key harus menjadi property nyata dari tipe item.
type User = {
id: string;
email: string;
role: "admin" | "editor";
score: number;
};
function uniqueBy<T>(items: readonly T[]): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key?: K): T[] {
const seen = new Set<unknown>();
const output: T[] = [];
for (const item of items) {
const value = key === undefined ? item : item[key];
if (seen.has(value)) continue;
seen.add(value);
output.push(item);
}
return output;
}
const users: User[] = [
{ id: "u_1", email: "masa@example.com", role: "admin", score: 92 },
{ id: "u_2", email: "editor@example.com", role: "editor", score: 88 },
{ id: "u_1", email: "masa+copy@example.com", role: "admin", score: 70 },
];
const byId = uniqueBy(users, "id");
const byRole = uniqueBy(users, "role");
// @ts-expect-error "missing" is not a key of User.
uniqueBy(users, "missing");
console.log(byId.map((user) => user.id));
console.log(byRole.map((user) => user.role));
Prompt yang baik: “gunakan overloads, batasi key ke keyof T, dan tambahkan @ts-expect-error untuk key yang tidak ada.” Jika tidak, Claude Code bisa menghasilkan key: string dan item[key as keyof T]. Kode itu terlihat rapi, tetapi risiko dipindahkan ke runtime.
Use case 2: tipe respons API tanpa optional yang kabur
Use case kedua adalah tipe respons API. Banyak codebase menulis data?: T dan error?: ApiError dalam satu interface. Ini terlihat praktis, tetapi caller harus terus mengecek apakah data ada, apakah error ada, atau apakah state menjadi tidak jelas. Discriminated union membuat state eksplisit: sukses punya data, gagal punya error, dan ok mempersempit tipe.
type ApiError = {
code: string;
message: string;
retryable: boolean;
};
type ApiResult<T, E extends ApiError = ApiError> =
| { ok: true; status: number; data: T; error?: never }
| { ok: false; status: number; error: E; data?: never };
type UserDto = {
id: string;
name: string;
plan: "free" | "pro";
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function parseUserResponse(json: unknown): ApiResult<UserDto> {
if (
isRecord(json) &&
typeof json.id === "string" &&
typeof json.name === "string" &&
(json.plan === "free" || json.plan === "pro")
) {
return {
ok: true,
status: 200,
data: { id: json.id, name: json.name, plan: json.plan },
};
}
return {
ok: false,
status: 422,
error: {
code: "INVALID_USER_RESPONSE",
message: "User response does not match the expected shape.",
retryable: false,
},
};
}
function unwrap<T, E extends ApiError>(result: ApiResult<T, E>): T {
if (result.ok) {
return result.data;
}
throw new Error(`${result.error.code}: ${result.error.message}`);
}
const parsed = parseUserResponse({ id: "u_1", name: "Masa", plan: "pro" });
const user = unwrap(parsed);
console.log(user.name.toUpperCase());
Pola ini cocok untuk Claude Code karena prompt dapat menjelaskan validasi runtime dan kontrak tipe sekaligus. Untuk konteks backend, baca juga pengembangan API dengan Claude Code dan testing API dengan Claude Code.
Use case 3: form state dengan mapped types
Use case ketiga adalah form state. Dari model bisnis, kita menurunkan state setiap field yang memiliki value, dirty, dan errors. Mapped types mencegah duplikasi nama field dan menjaga tipe nilai: email tetap string, seats tetap number, newsletter tetap boolean.
type FieldState<T> = {
value: T;
dirty: boolean;
errors: string[];
};
type FormState<T extends object> = {
[K in keyof T]: FieldState<T[K]>;
};
function createFormState<T extends object>(initial: T): FormState<T> {
const entries = Object.entries(initial).map(([key, value]) => [
key,
{ value, dirty: false, errors: [] },
]);
return Object.fromEntries(entries) as FormState<T>;
}
function setField<T extends object, K extends keyof T>(
state: FormState<T>,
key: K,
value: T[K],
): FormState<T> {
return {
...state,
[key]: { value, dirty: true, errors: [] },
} as FormState<T>;
}
type SignupForm = {
email: string;
seats: number;
newsletter: boolean;
};
const form = createFormState<SignupForm>({
email: "team@example.com",
seats: 2,
newsletter: true,
});
const updated = setField(form, "seats", 3);
// @ts-expect-error seats must be a number.
setField(form, "seats", "three");
console.log(updated.seats.value);
Cast setelah Object.fromEntries adalah titik review. Ini bukan mempercayai input luar, melainkan mendokumentasikan bahwa transformasi mempertahankan key yang sama walaupun TypeScript tidak bisa menginfer mapped type secara presisi. Minta Claude Code menjelaskan setiap cast.
Verifikasi dengan tsc dan type tests
Artikel generics harus bisa diverifikasi. Simpan contoh di examples/generics.ts, lalu jalankan compiler strict.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true,
"lib": ["ES2022", "DOM"]
},
"include": ["examples/**/*.ts"]
}
npm install --save-dev typescript
npx tsc --noEmit --strict --lib ES2022,DOM examples/generics.ts
Untuk tes tipe saja, gunakan assertion compile-time kecil. Tidak ada efek runtime, tetapi build gagal jika tipe hasilnya berubah.
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2)
? true
: false;
type Expect<T extends true> = T;
type PickReadonly<T, K extends keyof T> = {
readonly [P in K]: T[P];
};
type Account = {
id: string;
email: string;
seats: number;
};
type PublicAccount = PickReadonly<Account, "id" | "email">;
type PublicAccountCheck = Expect<
Equal<PublicAccount, { readonly id: string; readonly email: string }>
>;
const leaked: PublicAccount = {
id: "a_1",
email: "team@example.com",
// @ts-expect-error seats is intentionally not part of PublicAccount.
seats: 10,
};
console.log("Type checks are compile-time only.");
Template review untuk Claude Code
Setelah generate, gunakan Claude Code untuk review tipe secara spesifik.
Template 1: review helper generic
Review function TypeScript ini.
Tujuan: deduplikasi array input berdasarkan key yang dipilih.
Syarat: key harus K extends keyof T. Jangan gunakan any. Tambahkan @ts-expect-error untuk key yang tidak ada.
Output: masalah, kode yang diperbaiki, dan command tsc untuk verifikasi.
Template 2: review tipe respons API
Review tipe respons API ini.
Tujuan: sukses memiliki data, gagal memiliki error.
Syarat: hindari field optional kabur seperti data?: T. Pastikan ok mempersempit tipe.
Output: contoh caller aman, contoh gagal, dan type tests tambahan.
Template 3: review mapped types
Review mapped type ini.
Tujuan: turunkan field state dari model form.
Syarat: jelaskan keyof, T[K], readonly, optional properties, dan cast yang diperlukan.
Output: alur tipe, kasus rapuh, dan perbaikan minimum yang aman.
Template 4: audit tipe sebelum PR
Audit generics, conditional types, dan mapped types dalam diff ini.
Cek: any, Record terlalu luas, type parameter tidak perlu, kurang @ts-expect-error, dan kurang validasi runtime.
Output: blocker, perbaikan kecil, dan test tambahan menurut prioritas.
Pitfall yang sering muncul
| Pitfall | Yang rusak | Kebiasaan lebih aman |
|---|---|---|
Memakai any agar terlihat generic | Return type kehilangan informasi | Tangkap hubungan dengan T |
Menulis key sebagai string | Property yang tidak ada ikut compile | Gunakan K extends keyof T |
Terlalu sering memakai Record<string, unknown> | Property konkret menghilang | Gunakan object jika bukan dictionary |
| Semua field API dibuat optional | Caller tidak bisa percaya data atau error | Gunakan discriminated union |
| Cast tanpa penjelasan | Reviewer tidak bisa menilai aman atau tidak | Tulis invariant sebelum cast |
Perbedaan T extends object dan T extends Record<string, unknown> penting. Model form biasanya hanya perlu object. Helper dictionary dengan key string bebas mungkin memang butuh Record.
CTA: hubungkan type safety dengan jalur pendapatan
Generics bukan sekadar permainan sintaks. Jika tipe lemah di form, checkout, payload API, template produk, atau analytics event, jalur dari pembaca ke pelanggan bisa rusak. Mulai dari cheatsheet Claude Code gratis, gunakan produk dan template jika butuh prompt berulang, lalu gunakan training atau konsultasi Claude Code bila tim perlu menstandarkan CLAUDE.md, review tipe, CI, dan rollout.
Saat menerapkan contoh ini, cek dulu tipe yang paling dekat dengan bisnis: akun, billing, form, respons API, dan tracking. Tanyakan ke Claude Code bukan hanya “apakah compile?”, tetapi juga “apakah kesalahan tipe ini bisa merusak conversion path?”
Hasil setelah dicoba
Setelah mencoba workflow ini, Masa melihat hasil lebih stabil saat prompt implementasi dipisahkan dari prompt review tipe. Pertama buat helper, lalu minta Claude Code mengaudit any, keyof yang hilang, respons API yang terlalu optional, dan kurangnya @ts-expect-error. Contoh uniqueBy dan form state sangat berguna karena tsc --noEmit --strict membuktikan dua sisi kontrak: panggilan valid compile, panggilan yang sengaja salah ditolak.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Permission safety ladder Claude Code: perluas akses tanpa kehilangan kontrol
Naik dari read-only ke edit terbatas, command bukti, dan cek deploy dengan kontrol yang jelas.
Claude Code Small PR Proof Pack: perubahan kecil yang mudah direview
Paket bukti untuk PR Claude Code: diff, check, URL publik, jalur CTA, dan rollback.
Review gate Claude Code sebelum commit: diff, test, URL publik, dan CTA
Cara memakai Claude Code sebelum commit: diff scope, build, URL publik, link Gumroad, CTA konsultasi, missing test, dan file tidak terkait.