กลับไปที่บทความ
TypeScript Architecture Frontend Backend

TypeScript Patterns สำหรับ Large Codebases

พลากร วรมงคล
18 มีนาคม 2568 10 นาที

“TypeScript ที่ขนาดใหญ่ต้องการรูปแบบที่แตกต่างจาก TypeScript สำหรับ side project นี่คือรูปแบบ type และการตัดสินใจทางสถาปัตยกรรมที่เก็บ large codebases ไว้ให้ maintainable ตามการจัดการโปรเจกต์ TypeScript 200K+ line”

TypeScript ที่ Scale แตกต่างกัน

เมื่อ TypeScript codebase ของคุณอยู่ภายใต้ 10K lines เกือบทุกวิธีใช้ได้ Types เป็น loose exports เป็น casual และ refactoring ใช้ได้ง่ายเพราะคุณสามารถจดจำ entire codebase ในหัวของคุณ ที่ 50K+ lines ที่มี multiple contributors การตัดสินใจที่คุณทำตั้งแต่เริ่มต้นเริ่มทบต้น — ดีหรือร้าย

ฉันได้เก็บรักษา TypeScript codebases 3 อันที่เกิน 200K lines of code และรูปแบบที่สำคัญที่ scale แตกต่างจาก tutorials สอน

รูปแบบ 1: Branded Types สำหรับ Domain Safety

Primitive obsession — ใช้ plain strings และ numbers ทุกแห่ง — เป็น single biggest source ของ bugs ใน large TypeScript codebases เมื่อ function ของคุณยอมรับ userId: string ไม่มีสิ่งใดหยุด pass orderId: string TypeScript’s structural typing หมายความว่า both เป็น strings

Branded types แก้ไขนี้โดยสร้าง nominal type distinctions UserId ไม่สามารถส่งต่อไปยังที่ OrderId คาดหวัง แม้ว่า both เป็น strings at runtime runtime cost zero — พวกเขาเป็น purely compile-time guardrails

รูปแบบนี้จับ bugs ที่ unit tests หลายครั้ง miss: ส่ง wrong ID ไปยัง function mixing up currency amounts หรือสับสน timestamps ที่มี different semantics

รูปแบบ 2: Discriminated Unions สำหรับ State Machines

ทุก non-trivial application มี state machines: request เป็น loading succeeded หรือ failed user เป็น active suspended หรือ deleted order เป็น pending paid shipped หรือ cancelled

Discriminated unions จำลองเหล่านี้อย่างสมบูรณ์แบบ แทนที่จะเป็น single type ที่มี optional fields (status: string, data?: T, error?: Error) สร้าง union ที่ variant แต่ละรายการมีแน่นอน fields ที่สำคัญสำหรับสภาวะนั้น

Compiler จึง enforces exhaustive handling — ถ้าคุณเพิ่ม new state ทุก switch statement ที่จัดการ union จะ error จนกว่าคุณจะจัดการ new case นี่เป็น incredibly valuable ใน large codebase ที่ state handling logic กระจายข้าม dozens files

รูปแบบ 3: Strict Module Boundaries

ใน large codebase ไม่ใช่ทุกไฟล์ที่ควร importable จากทุกแห่ง สร้าง module boundaries โดยใช้ barrel exports (index.ts files) ที่อย่างชัดเจน declare module’s public API

internal directory structure ของ module คือ implementation detail external consumers import จาก module’s index ไม่ใช่จาก internal files นี่หมายความว่าคุณ refactor internals โดยไม่ breaking consumers และ module’s public API ของคุณ documented โดย exports

Enforce นี้ด้วย ESLint rules ที่ป้องกัน deep imports ข้าม module boundaries บาง teams ใช้ TypeScript path aliases ทำให้ module boundaries ชัดเจนยิ่งขึ้น

รูปแบบ 4: Generic Constraints สำหรับ API Layers

API layer ของคุณควร typed end-to-end จาก HTTP handler ไปยัง database query Generic constraints ทำให้เป็นไปได้โดยไม่ duplicating types

define API contract ของคุณเป็น type ที่ map routes ไปยัง request/response types จากนั้น write generic functions ที่ accept route key และ automatically infer correct request และ response types นี่ให้ type safety จาก API client ผ่าน handler ไปยัง database ด้วย types defined ใน single place

เมื่อคุณ change API response type ทุก consumer ที่ใช้มันไม่ถูกต้องจะ fail at compile time ใน large codebase ที่มี dozens API endpoints นี่ป้องกัน entire class ของ integration bugs

รูปแบบ 5: Utility Types สำหรับ Transformation

TypeScript’s built-in utility types (Pick, Omit, Partial, Required) มีประสิทธิภาพแต่ไม่เพียงพอสำหรับ complex transformations สร้าง library ของ domain-specific utility types

Common patterns รวม: DeepPartial สำหรับ nested optional fields ใน update operations StrictOmit ที่ errors ถ้าคุณพยายาม omit key ที่ไม่มี Prettify ที่ flatten intersections สำหรับ readable hover types และ NonEmptyArray สำหรับ arrays ที่ต้องมี at least one element

Utility types เหล่านี้ encode business rules ที่ type level function ที่ requires NonEmptyArray parameter ไม่สามารถ receive empty array ขจัด class ของ runtime errors

รูปแบบ 6: Type-Safe Event Systems

Large applications มักจะใช้ event-driven patterns สำหรับ cross-module communication ไม่มี types event systems กลายเป็น maintenance nightmare — คุณกำลัง passing untyped payloads ผ่าน string-keyed channels

Define type map ที่ associates event names ด้วย payload types จากนั้น write generic emit และ listen functions ที่ infer correct payload type จาก event name เพิ่ม new event หมายความว่า adding one entry ไปยัง type map และ all emitters และ listeners ได้รับ correct types automatically

รูปแบบ 7: Const Assertions สำหรับ Configuration

Large codebases มี lots of configuration: feature flags route definitions permission matrices error codes ใช้ const assertions (as const) turn เหล่านี้จาก loose types เป็น literal types

configuration object ที่ defined ด้วย as const ให้ exact string literal types สำหรับ values ซึ่ง enables exhaustive checking autocomplete และ type narrowing ที่ loose string types ไม่สามารถให้ไป

รูปแบบ 8: Template Literal Types สำหรับ String Patterns

TypeScript 4.1 introduced template literal types ที่ให้คุณ define patterns สำหรับ strings นี่เป็น invaluable สำหรับ APIs ที่ follow naming conventions

คุณสามารถ define types สำหรับ API routes CSS class patterns translation keys หรือ any other string ที่ follow predictable format compiler enforces pattern ที่ทุก usage site ไป catching typos และ convention violations at compile time

Compiler Configuration สำหรับ Scale

Strict Mode Is Non-Negotiable

Enable strict: true จาก day one Retrofitting strict mode ไปยัง large codebase เป็นปวดและ expensive individual flags ที่มันเปิดใช้งาน (strictNullChecks, noImplicitAny, strictFunctionTypes, etc.) แต่ละ catch real bugs ที่ would otherwise reach production

Incremental Compilation

Enable incremental: true และ tsBuildInfo output ใน 200K line codebase นี่สามารถ reduce recompilation time จาก 30 วินาที ไปยัง 3 วินาที compiler caches type information และเพียง rechecks changed files และ dependents ของพวกเขา

Project References

สำหรับ monorepos ใช้ TypeScript project references เพื่อ break codebase ของคุณเข้าไป independently compilable units ทุก package ได้รับ own tsconfig.json ด้วย references array compiler builds packages ใน dependency order และ caches results

Testing Types

อย่าเพียงแค่ test runtime behavior — test types ของคุณ Libraries เช่น ts-expect-error comments conditional types ที่ resolve ไปยัง never สำหรับ invalid inputs และ dedicated type testing tools ให้คุณ verify ที่ types ของคุณ catch errors ตามที่คาดหวัง

นี่เป็นอย่างยิ่ง important สำหรับ utility types และ generic functions type ที่ควรมี reject invalid input ควรมี tests proving ที่ invalid input causes compile error

The Long Game

TypeScript ที่ scale เกี่ยวกับ leverage: ทุก hour ที่ลงทุนใน good types บันทึก hundreds hours debugging code review และ runtime errors ข้าม team รูปแบบข้างบนไม่เกี่ยวกับ type gymnastics สำหรับ own sake — พวกเขา encode business rules ป้องกัน real bugs และทำให้ refactoring ปลอดภัย ใน codebases เกินไปใหญ่สำหรับใคร one person เพื่อเข้าใจอย่างเต็มที่

Comments powered by Giscus are not yet configured. Set PUBLIC_GISCUS_REPO_ID and PUBLIC_GISCUS_CATEGORY_ID in apps/web/.env to enable.

PV

เขียนโดย พลากร วรมงคล

Software Engineer Specialist ประสบการณ์กว่า 20 ปี เขียนเกี่ยวกับ Architecture, Performance และการสร้างระบบ Production

เพิ่มเติมเกี่ยวกับผม

บทความที่เกี่ยวข้อง