TypeScript untuk Laravel developer adalah langkah alami: kalau kamu sudah menulis PHP dengan type hint, enum, dan readonly class, konsep TypeScript hampir sama, hanya dipakai di sisi JavaScript. Manfaat terbesarnya terasa saat frontend mengonsumsi API Laravel: salah ketik nama field, data yang bisa null, dan bentuk respons yang berubah langsung ketahuan saat menulis kode, bukan saat pengguna melapor error.

Setup TypeScript di Proyek Laravel

Starter kit Laravel terbaru untuk Vue dan React sudah memakai TypeScript. Untuk proyek yang sudah ada:

npm install -D typescript
npx tsc --init

Ubah entry point di vite.config.js dari resources/js/app.js ke resources/js/app.ts, lalu ganti pemanggilan di Blade:

@vite(['resources/css/app.css', 'resources/js/app.ts'])

Vite bisa langsung memproses file .ts. Pengecekan tipe dijalankan terpisah dengan npx tsc --noEmit, sebaiknya di CI. Pengaturan awal yang disarankan di tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "paths": { "@/*": ["./resources/js/*"] }
  },
  "include": ["resources/js/**/*"]
}

Padanan Konsep dari PHP ke TypeScript

PHPTypeScript
function total(int $qty): intfunction total(qty: number): number
?string $notenote: string | null
interface Payableinterface Payable (hanya ada saat kompilasi)
enum Status: stringtype Status = 'draft' | 'paid'
readonly class Moneyreadonly pada properti atau Readonly<T>
Array shape di PHPDoctype atau interface untuk objek
Generic di PHPDoc (Collection<int, User>)Generic bawaan: Array<User>

Perbedaan penting: tipe TypeScript hilang setelah dikompilasi. Tidak ada pengecekan tipe saat runtime, jadi data dari luar (API, input pengguna) tetap perlu divalidasi.

Mengetik Respons API Laravel

Misalkan API mengembalikan resource seperti contoh di REST API dengan Laravel Sanctum:

// resources/js/types/order.ts
export type OrderStatus = 'menunggu' | 'dibayar' | 'dikirim' | 'selesai';

export interface Order {
  id: number;
  number: string;
  total: number;
  status: OrderStatus;
  created_at: string;
}

export interface Paginated<T> {
  data: T[];
  links: { next: string | null; prev: string | null };
  meta: { current_page: number; last_page: number; total: number };
}
// resources/js/api/orders.ts
import type { Order, Paginated } from '@/types/order';

export async function fetchOrders(page = 1): Promise<Paginated<Order>> {
  const response = await fetch(`/api/orders?page=${page}`, {
    headers: { Accept: 'application/json' },
    credentials: 'same-origin',
  });

  if (!response.ok) {
    throw new Error(`Gagal memuat pesanan (${response.status})`);
  }

  return response.json() as Promise<Paginated<Order>>;
}

Sekarang editor akan menolak order.totl atau perbandingan order.status === 'paid' karena nilai itu tidak ada di tipe.

Validasi saat runtime

Untuk data yang kritis, validasi bentuknya saat runtime dengan pustaka seperti Zod, sehingga tipe dan validasi berasal dari satu definisi:

import { z } from 'zod';

const OrderSchema = z.object({
  id: z.number(),
  number: z.string(),
  total: z.number(),
  status: z.enum(['menunggu', 'dibayar', 'dikirim', 'selesai']),
  created_at: z.string(),
});

export type Order = z.infer<typeof OrderSchema>;

Menjaga tipe tetap sinkron dengan backend

Tipe yang ditulis manual bisa tertinggal saat backend berubah. Beberapa pendekatan: menghasilkan tipe dari enum dan resource PHP dengan paket seperti spatie/laravel-typescript-transformer, atau dari spesifikasi OpenAPI. Untuk rute, Laravel Wayfinder bisa menghasilkan fungsi TypeScript dari route dan controller.

Form dengan Tipe

interface CheckoutForm {
  name: string;
  phone: string;
  courier: 'jne' | 'jnt' | 'sicepat';
  note?: string;
}

type ValidationErrors<T> = Partial<Record<keyof T, string[]>>;

// respons 422 dari Laravel: { message, errors: { field: [pesan] } }
function firstError(errors: ValidationErrors<CheckoutForm>, field: keyof CheckoutForm): string | undefined {
  return errors[field]?.[0];
}

Dengan Vue atau React

<script setup lang="ts">
import type { Order } from '@/types/order';

const props = defineProps<{ order: Order }>();
const emit = defineEmits<{ cancel: [orderId: number] }>();
</script>
type OrderRowProps = { order: Order; onCancel: (id: number) => void };

export function OrderRow({ order, onCancel }: OrderRowProps) {
  return <button onClick={() => onCancel(order.id)}>Batalkan {order.number}</button>;
}

Kesalahan yang Sering Terjadi

  • Memakai any di mana-mana, yang mematikan manfaat TypeScript. Gunakan unknown lalu persempit tipenya.
  • Mematikan strict agar error hilang. Lebih baik migrasi bertahap per file.
  • Mengira tipe memvalidasi data. Respons API yang berubah tetap bisa membuat aplikasi error saat runtime.
  • Tidak menjalankan tsc di CI, sehingga error tipe lolos karena Vite tidak memeriksa tipe saat build.

Langkah Berikutnya

Kalau kamu ingin memakai TypeScript di framework full-stack JavaScript, lihat Next.js untuk backend developer. Untuk runtime yang menjalankan TypeScript langsung tanpa langkah kompilasi, baca Bun vs Node.js.