หากคุณเคยส่ง bugfix เพียงสองบรรทัดไปยัง TypeScript monorepo และต้องนั่งดู build pipeline ใช้เวลาถึงสิบนาทีในการทำ type checking คุณคงเข้าใจปัญหาดีแล้ว ความล่าช้านี้ไม่ใช่เรื่องของการ bundling ไม่ใช่การทำ minification แต่มันคือช่วงเวลาที่ TypeScript พยายามเขียน declaration files ของคุณ ก่อนที่มันจะสามารถ emit ไฟล์ .d.ts ออกมาได้แม้แต่ไฟล์เดียว ตัว compiler จะต้องทำการ resolve type graph ทั้งหมดเสียก่อน มันต้องไล่ดู imports, ประเมิน generics และหา return types ที่อาจจะซ่อนอยู่ลึกถึงสามชั้นของ dependency ไฟล์ที่คุณแก้ไขเพียงไฟล์เดียวอาจไปกระทบกับอีกไฟล์หนึ่ง ซึ่งไปกระทบกับไฟล์ที่สาม และทันใดนั้น compiler ก็ต้องสวมบทนักสืบไล่ตรวจสอบไปทั่วทั้ง repository เพียงเพื่อจะอธิบายว่าฟังก์ชันของคุณคืนค่าอะไรออกมา

TypeScript 6.0 ได้แนะนำ isolatedDeclarations เพื่อทำลายพฤติกรรมดังกล่าว

คอขวดที่แท้จริง

Declaration files คือสัญญาที่เป็นสาธารณะ (public contract) ของโค้ดคุณ เมื่อนักพัฒนาคนอื่นทำการ import package ของคุณ TypeScript จะอ่านไฟล์ .d.ts ไม่ใช่ไฟล์ source การจะสร้างไฟล์เหล่านั้นให้ถูกต้องหมายความว่า compiler ไม่สามารถข้ามขั้นตอนการทำ type checker ได้ มันต้องรู้ถึงทุก shape, ทุก union และทุก inferred return type ก่อนที่จะเขียน output ออกมาแม้แต่บรรทัดเดียว

ในโปรเจกต์ขนาดเล็กที่มีไฟล์เพียงห้าสิบไฟล์ เรื่องนี้จะเกิดขึ้นแทบจะทันที แต่ใน monorepo ขนาดใหญ่ที่มีโมดูลนับพัน มันคือฝันร้ายแบบ serialized หากคุณเปลี่ยน utility type ใน core package ตัว compiler จะต้องกลับไปตรวจสอบทุก consumer เพื่อยืนยันว่า inferred types เหล่านั้นยังคงถูกต้องอยู่ เวลาในการ build จะเพิ่มขึ้นตามความลึกของ dependency ไม่ใช่แค่จำนวนไฟล์ การ refactor เล็กๆ น้อยๆ อาจกระตุ้นให้เกิดการคำนวณใหม่ทั้ง graph (full-graph recomputation) ทีมพัฒนาส่วนใหญ่มักยอมรับว่าสิ่งนี้เป็นเรื่องที่หลีกเลี่ยงไม่ได้ แต่จริงๆ แล้วมันไม่ใช่

isolatedDeclarations ทำงานอย่างไร

flag ใหม่นี้จะเปลี่ยนสัญญา (contract) ระหว่างคุณกับ compiler แทนที่จะขอให้ TypeScript ทำการ infer types ของ exports คุณจะเป็นคนระบุ (annotate) พวกมันด้วยตัวเอง การเปลี่ยนแปลงเพียงจุดเดียวนี้ช่วยขจัดความจำเป็นในการต้องรู้ข้อมูลแบบ global ออกไป compiler ไม่จำเป็นต้องวิเคราะห์ dependencies ของคุณอีกต่อไปเพื่อ emit declarations สำหรับโมดูลของคุณ แต่มันจะดูเพียงแค่ syntax ที่อยู่ตรงหน้ามันเท่านั้น

นั่นหมายความว่าแต่ละไฟล์สามารถ emit output .d.ts ของตัวเองได้อย่างเป็นอิสระและขนานกัน (in parallel) เครื่องมือ build ไม่จำเป็นต้องสร้าง type graph ที่สมบูรณ์ก่อนเริ่มการสร้าง declaration เครื่องมืออย่าง esbuild และ swc ซึ่งก่อนหน้านี้หลีกเลี่ยงการ emit .d.ts เพราะขาด type checker ตอนนี้สามารถสร้าง declarations ได้ด้วยความเร็วที่ใกล้เคียงกับการทำ transpilation พวกมันจะอ่าน explicit annotations โดยตรงจาก source และเขียน type definitions ที่เกี่ยวข้องออกมาโดยไม่ต้องแก้สมการทาง type (type equations) ใดๆ

ก่อนหน้านี้ การ emit declaration เป็นการผูกขาดโดย tsc เพราะมีเพียง compiler อย่างเป็นทางการเท่านั้นที่มีข้อมูล type ที่จำเป็นในการสร้างไฟล์ .d.ts ตัว fast transpilers อาจจะสามารถ strip types หรือแปลง syntax ได้ แต่ไม่สามารถสร้าง type definitions ได้ แต่ด้วย isolatedDeclarations ประตูได้เปิดกว้างให้ทั้ง ecosystem สามารถจัดการ workflow ของการสร้าง declaration ได้ การแปลงข้อมูลจะกลายเป็นเรื่องเชิงกลไก (mechanical) มากกว่าเชิงตรรกะ (logical) และความแตกต่างนี้เองที่เปลี่ยนการรอคอยสิบนาทีให้เหลือเพียงไม่กี่วินาที

ข้อแลกเปลี่ยน: ความชัดเจน (Explicit) คือมาตรฐานใหม่

ความเร็วมาพร้อมกับราคาที่ต้องจ่ายอย่างตรงไปตรงมา ทุกๆ exported function, class และ variable จะต้องมีการระบุ explicit type annotation ทุกฟังก์ชัน TypeScript จะปฏิเสธที่จะ infer public API ให้คุณ

ลองพิจารณา utility ง่ายๆ ดังนี้:

export function getUser(id: number) {
  return fetchUser(id);
}

เมื่อเปิดใช้งาน isolatedDeclarations โค้ดนี้จะเกิด error เนื่องจาก compiler ไม่สามารถทราบ return type ของ fetchUser ได้โดยไม่ต้องตรวจสอบมัน และภายใต้ flag นี้