Kamu baru aja bump TypeScript dari 5.3 ke 5.5. Semua unit test lewat. Tapi begitu CI pipeline jalan, 147 error muncul dari Zod schema yang kemarin masih mulus. Padahal kamu cuma ganti versi compiler, bukan kode validasinya. Tangan mulai dingin. “Apa yang berubah?”

⚡ Jawaban Singkat / Key Takeaways

TypeScript 5.4+ memperketat inferensi template literal type di posisi yang sebelumnya “longgar.” Hasilnya: Zod z.string().min(1), Yup string().required(), dan type-safe router seperti TanStack Router yang bergantung pada branded string type bisa tiba-tiba menghasilkan error Type 'string' is not assignable to type.... Kamu tidak salah koding. Compiler-nya yang berubah. Artikel ini jelasin akar masalah, kenapa changelog resmi nggak menyebut ini sebagai breaking change, dan tiga workaround produksi yang bisa kamu pakai hari ini.

Apa yang Sebenarnya Berubah: Template Literal Inference yang Semakin Ketat

Masalahnya bermula dari sebuah pull request TypeScript yang judulnya terdengar innocuous: “Improve inference from template literal types.” Singkatnya, TS dulu menginfer string literal dari operasi string biasa. Sekarang, compiler lebih konservatif. Ia menolak menginfer tipe literal dari value yang tidak bisa diverifikasi secara compile-time.

Dampaknya brutal. Pertimbangkan kode Zod sederhana ini:

import { z } from 'zod';

const UserSchema = z.object({
  role: z.enum(['admin', 'user', 'guest']),
  token: z.string().brand('AccessToken'),
});

// TypeScript 5.3: ✅ OK
// TypeScript 5.5: ❌ Type 'string' is not assignable to 
//   type 'Branded<string, "AccessToken">'
const validated = UserSchema.parse(input);
const t: string = validated.token; // token kehilangan brand-nya!
Kode TypeScript dengan error squiggle merah akibat stricter template literal inference pada schema Zod
Error squiggle merah mulai bermunculan setelah upgrade TypeScript

Branded type yang tadinya terjaga oleh z.string().brand() tiba-tiba diinfer sebagai string biasa. Compiler tidak bisa “melihat” bahwa runtime parsing akan menghasilkan branded string. Ini terjadi karena inference pada template literal gagal mempertahankan brand saat melewati generic chain Zod.

Tiga Perpustakaan yang Paling Kena Dampak

1. Zod: Branded String dan Refinement Type Ambyar

Zod versi di bawah 3.23 sangat bergantung pada template literal inference untuk mempertahankan branded types, .refine() narrow type, dan .pipe() coercion pipeline. Begitu inference tightened, narrow type dari .superRefine() bisa “jatuh” kembali ke tipe dasar.

  • Error yang muncul: Type 'string' is not assignable to type 'Branded<...>'
  • Akar masalah: Zod's internal ZodType generic kehilangan precision di chain pipe vs refine
  • Versi terdampak: Zod < 3.23.8 dengan TS >= 5.4
Zod schema validation error akibat stricter template literal type inference TypeScript 5.4 ke atas
Zod schema yang ambyar setelah TS update: validasi runtime tetap jalan, tapi type inference-nya rusak

2. Type-Safe Router: TanStack Router & tRPC

Router seperti TanStack Router menggunakan template literal types untuk membangun path matching yang fully typed. Pattern seperti /posts/${string} dan branded parameter ${'postId'}-${string} bisa gagal infer saat melewati nested generic.

// TanStack Router: path parameter inference
const postRoute = createRoute({
  path: '/posts/${postId}',
  // TS 5.5: postId diinfer sebagai string, bukan branded PostId
  loader: ({ params }) => fetchPost(params.postId),
});

3. Yup & io-ts: Validation Schema Inference

Yup dan io-ts juga terkena, terutama di chain .transform() yang menghasilkan branded type dari string literal. Error lebih samar karena Yup tidak punya type inference se-eksplisit Zod. Tapi kalau kamu pakai InferType<typeof schema>, narrowed types bisa meluruh.

Kenapa Changelog Resmi Gloss Over Masalah Ini

Ini bagian yang bikin frustrasi. Di changelog TypeScript 5.5, kamu lihat section “Inferred type predicates” dan “Control flow narrowing,” tapi tidak ada satu paragraf pun yang eksplisit menyebut “template literal inference menjadi lebih ketat.” Kenapa?

  • Ini “fix,” bukan “breaking change” menurut tim TS. Inferensi sebelumnya dianggap terlalu longgar dan bisa menghasilkan false positive type safety.
  • Dampaknya hanya ke library dengan generic chain kompleks, bukan ke kode aplikasi langsung. Tim TS menganggap library author yang harus beradaptasi.
  • PR-nya adalah PR#57644, yang judulnya “Narrowing from generic conditional types” – tidak ada kata “template literal” di judulnya. Developer yang browsing changelog tidak akan menghubungkan ini dengan schema validation library mereka.

Akibatnya, library consumer seperti kamu yang kena dampaknya. Bukan library author-nya.

Kapan Inference Gagal Itu Normal (dan Kapan Kamu Harus Curiga)

Ini trik dari production debugger veteran: tidak semua kegagalan inference adalah bug compiler. Ada satu pola spesifik yang hampir selalu menjadi akar masalah. Namanya “generic chain collapse.”

Generic chain collapse terjadi saat tipe melewati tiga atau lebih generic wrapper tanpa explicit return type annotation. Compiler “lelah” dan mulai menginfer tipe terluas yang aman: string alih-alih 'admin' | 'user', atau Record<string, unknown> alih-alih { role: 'admin' }.

Pola deteksi: Kalau error hanya muncul setelah melewati tiga layer Zod chain (.pipe().refine().transform()), itu generic collapse. Kalau error muncul di .parse() pertama, itu masalah di inference literal string langsung. Dua kasus ini butuh workaround yang berbeda.

Definisi type-safe router TypeScript yang broken setelah upgrade compiler akibat template literal inference tightened
Router type-safe ikut rusak karena path parameter inference melemah

Workaround #1: Explicit Return Type Annotation di Schema-mu

Cara paling straightforward, dan seringkali cara yang direkomendasikan Zod 3.23+, adalah memberi explicit type annotation pada schema:

// Sebelum (TS 5.3 oke, TS 5.5 merah)
const UserSchema = z.object({
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

// Sesudah: beri explicit output type
const UserSchema: z.ZodType<{
  email: string;
  role: 'admin' | 'user';
}> = z.object({
  email: z.string().email(),
  role: z.enum(['admin', 'user']),
});

Ini menghentikan generic collapse sebelum mulai. Compiler tidak perlu menginfer dari chain panjang; ia tinggal mengecek bahwa schema yang kamu tulis cocok dengan annotated type.

Workaround #2: Gunakan Zod 3.23+ dengan z.output Internal Type

Zod 3.23.8 memperkenalkan internal type z.output<typeof schema> yang lebih tahan terhadap perubahan inference TS. Kalau kamu selama ini pakai z.infer<typeof schema> untuk menghasilkan DTO type dari schema, coba ganti ke:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid().brand('UserId'),
  name: z.string().min(1),
});

// Gunakan z.output, bukan z.infer
type UserDTO = z.output<typeof UserSchema>;
// Brand 'UserId' tetap terjaga di TS 5.5

z.output mem-bypass beberapa heuristic inference yang diubah oleh TS 5.4+. Perbedaan spesifiknya ada di cara Zod menangani ZodBranded di internal typedef, tapi dampaknya langsung terasa di kode produksimu.

Workaround #3: Narrowing Manual via Type Guard Function

Untuk kasus di mana kamu butuh branded type yang benar-benar robust, buat type guard function sendiri:

// Definisikan branded type secara eksplisit
type AccessToken = string & { readonly __brand: 'AccessToken' };

// Buat type guard yang tidak bergantung pada template literal
function isAccessToken(value: string): value is AccessToken {
  return value.startsWith('at_') && value.length === 48;
}

// Pakai di Zod dengan .refine() + explicit cast
const TokenSchema = z.string().refine(
  (v): v is AccessToken => isAccessToken(v),
  { message: 'Invalid access token format' }
);

Pendekatan ini menghindari template literal sama sekali. Brand disematkan lewat intersection type manual, bukan lewat template literal inference. Workaround ini bekerja di TypeScript versi mana pun, dan merupakan defensive coding pattern yang direkomendasikan untuk library publik.

Cara Cek Apakah Codebase-mu Terdampak Sebelum Upgrade

Jangan tunggu CI merah. Jalankan audit ini di laptopmu:

# 1. Install TypeScript 5.5 secara lokal (tanpa commit)
npm install [email protected] --save-dev

# 2. Jalankan type check tanpa emit
npx tsc --noEmit 2>&1 | grep -c "is not assignable"

# 3. Filter error Zod/Yup/tRPC saja
npx tsc --noEmit 2>&1 | grep -E "(zod|yup|trpc|tanstack)" | wc -l

Kalau hitungan error di atas 10, codebase-mu terdampak. Rollback dulu, baca workaround di atas, baru upgrade setelah semua error hijau.

FAQ: Template Literal Inference Breaking Change

Kenapa TypeScript tiba-tiba mempersempit type inference di template literal?

Tim TypeScript menganggap inferensi sebelumnya “terlalu optimis.” Compiler sering mengasumsikan bahwa string dari runtime akan selalu match dengan template literal yang didefinisikan, padahal di runtime bisa saja tidak. Perubahan ini adalah bagian dari inisiatif larger “soundness improvement” yang dimulai sejak TS 4.9.

Apakah downgrade TypeScript adalah solusi yang aman?

Untuk jangka pendek, ya. Pin TypeScript ke 5.3.3 di package.json dan lanjutkan development. Tapi ini bukan solusi jangka panjang. Library seperti Zod terus memperbarui typedef mereka untuk TS terbaru, dan kamu akan ketinggalan fitur seperti isolatedDeclarations dan erasableSyntaxOnly. Baca artikel kami tentang migrasi isolatedDeclarations untuk gambaran lebih lengkap.

Apakah ini cuma terjadi di Zod? Bagaimana dengan library validasi lain?

Semua library validasi yang menghasilkan branded type dari template literal inference terpengaruh. Zod paling terdampak karena branded type adalah first-class citizen di Zod. Lalu Yup dan io-ts juga kena, tapi biasanya di chain .transform(). tRPC dan TanStack Router terpengaruh di path inference. Valibot dan ArkType belum banyak terdampak karena arsitektur type inference-nya berbeda.

Jangan Biarkan Compiler Baru Membunuh Production-mu

Template literal inference yang diperketat adalah contoh sempurna dari breaking change yang tersembunyi di balik kata “improvement.” Changelog tidak menyebutnya sebagai breaking, tapi codebase-mu yang membayar harganya. Pahami tiga workaround di atas, audit codebase sebelum upgrade, dan selalu pin versi compiler di production.

Kalau kamu maintain library publik, pertimbangkan untuk menambahkan defensive type annotation dan menghindari generic chain yang terlalu panjang. Pengguna library-mu akan berterima kasih saat mereka upgrade TypeScript dan tidak melihat satu pun error merah.

Baca juga artikel kami yang lain: TypeScript 5.5 Bikin Kode React Diet 40% untuk tahu fitur baru yang justru menyelamatkan DX kamu.

Referensi

About the Author

Dzul Qurnain

Suka nonton Anime, ngoding dan bagi-bagi tips kalau tahu.. Oh iya, suka baca ( tapi yang menarik menurutku aja)... Praktisi WordPress, web development, SEO, dan server administration yang membagikan tutorial teknis dan catatan implementasi nyata.

View All Articles