Skip to main content

Functional Programming ที่ซ่อนอยู่ใน TypeScript Types — เรียน FP ผ่าน type system ไม่ต้องเขียน Haskell

· 11 min read

บันทึก — 25 สิงหาคม 2569 — เช้ามืด เจอกระทู้น่าสนใจ

มีคนโพสต์ใน r/typescript ว่า "ผมเล่นกับ TypeScript types แล้วสนุกมาก ขอแชร์" — พร้อมบล็อก 2 ตอน: primer เรื่อง FP ใน typesystem + Part 1 ที่ patch compiler เพื่อบวกเลขคี่ 9 ตัว

ผมอ่านแล้วชอบมาก เพราะมันทำให้ผมเห็น TypeScript ในมุมใหม่ — type system ไม่ใช่แค่ตัวช่วย type-check แต่เป็นภาษา FP ที่ซ่อนอยู่ในอีกภาษา และมัน Turing complete

บล็อกนี้ผมจะสรุป toolkit + ตัวอย่างจริง + optimization ที่คนใน comment แนะนำ + มุมมองส่วนตัวว่ามัน relevant กับงาน dev ปกติยังไง

TL;DR​

  • TypeScript types มี functions (generic types), pattern matching (extends ? :), infer, recursion, lists (tuples) — ครบเครื่อง FP
  • ใช้ได้จริงในโปรเจกต์ทั่วไป — type-safe state machines, parse query strings, validate API responses
  • ถ้าไปไกลถึงขั้น patch compiler (--noRecursionLimits) — ก็ทำได้ แต่อยู่ในโซน niche ("compile-time crimes")
  • คอมเมนต์เด่น: pattern BuildTuple + Add ที่ efficient กว่า Peano numbers — เป็นภาษา FP จริงๆ

Type System = Functional Language ที่ซ่อนอยู่​

Hugo Vilela เขียนไว้ดีมาก:

"TypeScript's type system is a programming language hiding inside another programming language. It has all the tools, functional programming languages give us to write programs: Functions, pattern matching and even recursion."

หลังอ่านแล้วผมเห็นด้วย — มันไม่ใช่แค่ "type สำหรับ IDE" แต่เป็น DSL ที่ compile เสร็จแล้วหายไป ซึ่งมีทั้งหมดนี้:

Conceptใน TypeScript typesใน Haskell/FP ทั่วไป
Functiontype F<A> = ...f :: A -> B
Pattern matchingT extends X ? Y : Zcase x of ...
Type variablesinfer Vf :: forall a. a -> ...
Recursiontype R<T> = T extends ... ? R<...> : ...recursion ปกติ
Liststuples [H, ...T][a]
Numberstuple lengthInt

Toolkit ที่ Hugo แนะนำ (primer)​

1. Type functions = Generic Types​

type Maybe<A> = A | undefined

Maybe เป็น type function ที่รับ A แล้ว return A | undefined — Maybe<3> == 3 | undefined

2. Pattern matching พื้นฐาน​

type Result<T, E> = { tag: 'Ok', value: T } | { tag: 'Err', error: E }

type IsOk<T extends Result<any, any>> = T extends { tag: 'Ok' } ? true : false

type A = IsOk<{ tag: 'Ok', value: 1 }>
// ^? true
type B = IsOk<{ tag: 'Err', error: 'asdf' }>
// ^? false

เหมือน Haskell's:

isOk (Ok _) = True
isOk _ = False

3. infer — จับ pattern​

type GetValue<T extends Result<any, any>> =
T extends { tag: 'Ok', value: infer V }
? V
: undefined

infer V คือการ "จับ" ส่วนหนึ่งของ pattern แล้วเอาไปใช้ฝั่งขวาของ ?

4. Default types ทำให้ signature สะอาด​

type Ok<T = any> = { tag: 'Ok', value: T }
type Err<E = any> = { tag: 'Err', error: E }
type Result<T = any, E = any> = Ok<T> | Err<E>

// จาก T extends Result<any, any> → T extends Result
type IsOk<T extends Result> = T extends Ok ? true : false

5. Union distribution — ตัวที่หลายคนไม่รู้​

type A = IsOk<Result>
// ^? boolean ← กระจาย union ออก

ถ้าอยากถาม "about the union itself" ให้ wrap ทั้งสองข้างใน tuple:

type IsOkStrict<T extends Result> = [T] extends [Ok] ? true : false

type A = IsOkStrict<Result> // ^? false
type B = IsOkStrict<Ok<1>> // ^? true

[T] ไม่ใช่ union → ไม่กระจาย → match ทั้งก้อน

6. Recursion + accumulator​

type CountOks<T extends Result[], C extends unknown[] = []> =
T extends [infer Head extends Result, ...infer Tail extends Result[]]
? IsOk<Head> extends true
? CountOks<Tail, [unknown, ...C]> // Ok → push
: CountOks<Tail, C> // Err → skip
: C['length'] // เสร็จ → อ่าน length

type A = CountOks<[Ok<1>, Err<'nope'>, Ok<3>]>
// ^? 2

นี่คือ tail recursion ที่มี default arg เป็น base case — เหมือน FP ทั่วไป

ตัวอย่างจริง: HackerRank "Sum of Odd Numbers" ใน Typesystem​

Hugo ลองทำโจทย์ HackerRank ใน typesystem — รับ array of numbers แล้ว sum ตัวที่เป็น odd

Input​

type Input = [3, 2, 4, 6, 5, 7, 8, 0, 1]
// expected output: 3 + 5 + 7 + 1 = 16

Step 1: Peano numbers — represent natural numbers​

type Zero = { readonly tag: 'Zero' };
type Succ<N extends Peano> = { readonly tag: 'Succ'; readonly prev: N };
type Peano = Zero | Succ<any>;

type One = Succ<Zero>
type Two = Succ<One>
type Three = Succ<Two>
// Three = { tag: 'Succ', prev: { tag: 'Succ', prev: { tag: 'Succ', prev: { tag: 'Zero' }}}}

Step 2: แปลง Peano ↔ Number​

// Peano → Number (อ่าน length ของ accumulator)
type ToNumber<N extends Peano, C extends unknown[] = []> =
N extends Zero ? C['length']
: N extends Succ<infer Prev>
? ToNumber<Prev, [unknown, ...C]>
: never;

// Number → Peano (สร้าง Peano ขนาด N)
type ToPeano<N extends number, Acc extends Peano = Zero, C extends unknown[] = []> =
C['length'] extends N
? Acc
: ToPeano<N, Succ<Acc>, [unknown, ...C]>;

Step 3: Add​

type Add<A extends Peano, B extends Peano> =
A extends Zero ? B
: A extends Succ<infer PrevA>
? Add<PrevA, Succ<B>> // ย้าย 1 จาก A ไป B
: never;

type A = ToNumber<Add<ToPeano<1>, ToPeano<2>>>
// ^? 3

Logic: ลด A ลงทีละ 1 แล้วเพิ่ม B ขึ้นทีละ 1 — เมื่อ A เป็น 0 ก็ return B

Step 4: IsOdd​

type IsOdd<N extends Peano> =
N extends Zero ? false
: N extends Succ<infer P1>
? P1 extends Zero
? true // predecessor = 0 → odd
: P1 extends Succ<infer P2>
? IsOdd<P2> // predecessor of predecessor → recurse
: never
: never;

Logic: ลบทีละ 2 ถ้าเจอ 0 = even, ถ้าเจอ 1 = odd

Step 5: Glue ทั้งหมดเข้าด้วยกัน​

type KeepOdd<N extends Peano> = IsOdd<N> extends true ? N : Zero;

type MapToPeano<T extends number[]> =
T extends [infer H extends number, ...infer Rest extends number[]]
? [ToPeano<H>, ...MapToPeano<Rest>]
: [];

type OddInput<T extends Peano[]> =
T extends [infer H extends Peano, ...infer Rest extends Peano[]]
? [KeepOdd<H>, ...OddInput<Rest>]
: [];

type Sum<T extends Peano[], Acc extends Peano = Zero> =
T extends [infer H extends Peano, ...infer Rest extends Peano[]]
? Sum<Rest, Add<H, Acc>>
: Acc;

type Result = ToNumber<Sum<OddInput<MapToPeano<Input>>>>
// ^? 16

สำเร็จ! แต่...

Step 6: Recursion limit เจอที่ 100 elements​

type Input = [3, 6, 9, 12, 15, ..., 100] // 100 elements
type Result = ToNumber<Sum<...>>
// ^? ERROR: Type instantiation is excessively deep and possibly infinite. [2589]

Root cause: Peano numbers มี complexity O(N) — ToPeano<100> = object ซ้อน 100 ชั้น แล้ว Add เดินทุกชั้น

Step 7: Patch compiler (ของจริง Hugo ทำ)​

Hugo clone TypeScript compiler แล้วเพิ่ม 2 flags:

  • --noRecursionLimits — เอา instantiation depth check ออก
  • --printType <name> — หา type <name> ในไฟล์แล้ว print type ออกมา

ผลลัพธ์:

$ time ./bin/tsc --noEmit --noRecursionLimits --printType Result peano_numbers.ts
2500
2.35s user 0.37s system 250% cpu 1.087 total

แค่บวกเลข 9 ตัว ใช้เวลา 2.35 วินาที — เพราะอยู่ในโซน "compile-time crimes" ไม่ใช่ production code

ถ้าใช้ string representation (O(log₁₀(N))) — เร็วขึ้น 4 เท่า: 0.57s

Optimization จาก comments​

คนใน r/typescript ชี้ optimization ที่ดีกว่า Peano numbers มาก:

1. IsOdd แบบ template literal (Merry-Lane)​

type IsOdd<N extends number> =
`${N}` extends `${string}${1 | 3 | 5 | 7 | 9}`
? true
: false;

type A = IsOdd<123>; // ^? true
type B = IsOdd<42>; // ^? false

ข้อดี: เร็วกว่า Peano มากเพราะไม่ต้อง recurse O(N) — ใช้ template literal เช็คตัวสุดท้ายของ string

ข้อเสีย: ไม่ functional — เป็น pattern matching on string ไม่ใช่ arithmetic

2. BuildTuple + Add แบบ tuple-length (Merry-Lane)​

type BuildTuple<
N extends number,
T extends unknown[] = []
> =
T['length'] extends N
? T
: BuildTuple<N, [...T, unknown]>;

type Add<A extends number, B extends number> =
[...BuildTuple<A>, ...BuildTuple<B>]['length'];

type X = Add<3, 5>;
// ^? 8

ข้อดี: ใช้ tuple length แทน nested objects — เร็วกว่า Peano numbers เพราะ TS optimize tuple length ได้ดีกว่า nested type instantiation

Insight: ตัวนี้คือ idiom ของ typescript-typelevel — คนที่เล่น typescript จะเจอบ่อยใน library อย่าง ts-essentials, type-fest

3. KeepOdd ปัญหา — map ไม่ใช่ filter (canarydev)​

มีคนชี้จุดบกพร่องของโค้ด Hugo:

type KeepOdd<N extends Peano> = IsOdd<N> extends true ? N : Zero;

"KeepOdd maps evens to Zero, instead of filtering them out. In your example it works because 0 is a no-op for a sum. But in a product, wouldn't this collapse to a 0?"

ถ้าเอาไปใช้กับ product แทน sum — even * 0 = 0 → ทุกอย่างเป็น 0

Fix แบบ filter จริงๆ (Scala-style):

type Filter<T extends Peano[]> =
T extends [infer H extends Peano, ...infer Rest extends Peano[]]
? IsOdd<H> extends true
? [H, ...Filter<Rest>] // เก็บไว้
: Filter<Rest> // ทิ้ง
: [];

Hugo ยอมรับ: "Yeah, you're right, would've been cleaner that way"

4. ทำไมไม่ patch TypeScript 7? (gbitten)​

"OP, why not hack TypeScript 7?"

ยังไม่มีคำตอบจาก Hugo — แต่ TS 7 มี architecture ใหม่ที่อาจจะ patch ยากกว่า หรืออาจจะมี feature ที่ทำให้ไม่ต้อง patch แล้วก็ได้

ใช้จริงในงาน dev ปกติได้ไหม?​

คำตอบสั้นๆ: ได้ แต่อยู่ในขอบเขต

ผมเห็น use case ที่ type-level FP คุ้มค่าจริงๆ:

1. Parse query string / form data แบบ type-safe​

// type-safe URL parser
type ParseQuery<T extends string> =
T extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key]: Value } & ParseQuery<Rest>
: T extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};

type Q = ParseQuery<'foo=bar&baz=qux'>
// ^? { foo: 'bar'; baz: 'qux' }

2. Type-safe state machines​

type State =
| { tag: 'idle' }
| { tag: 'loading' }
| { tag: 'success', data: string }
| { tag: 'error', error: Error };

type Transition<S extends State, E extends Event> =
S extends { tag: 'idle' } ? { tag: 'loading' }
: S extends { tag: 'loading' } ? { tag: 'success', data: string } | { tag: 'error', error: Error }
: S;

3. API response validators (compile-time)​

type ValidateShape<T, Expected> =
keyof T extends keyof Expected
? keyof Expected extends keyof T
? T
: { __error: 'Missing keys' }
: { __error: 'Extra keys' };

4. Auto-generate form types จาก schema​

type FormFields<T> = {
[K in keyof T]: T[K] extends string
? 'text' | 'email' | 'password'
: T[K] extends number
? 'number' | 'range'
: 'checkbox';
};

ทำไมถึงควรเรียนเรื่องนี้?​

ผมว่ามี 3 เหตุผล:

  1. มันเป็น FP ที่ apply ได้จริง — ถ้าเขียน TypeScript เป็นหลัก คุณเล่น FP ทุกวันอยู่แล้ว แค่ไม่รู้ตัว
  2. Compile-time safety — error ที่ typesystem จับได้ = runtime error ที่หายไป
  3. ปูพื้นฐาน FP — ถ้าอยากเรียน Haskell/Rust/Elm จริงๆ การเข้าใจ recursion + pattern matching + types เป็น skill ที่ transfer ได้

ส่วน "compile-time crimes" (patch compiler) นั่น niche มาก — ทำเพื่อความสนุก ไม่ใช่ production code

แล้ว TypeScript 7 ล่ะ?​

Hugo ยังไม่ตอบคำถาม gbitten ว่าจะ patch TS 7 ไหม — แต่ผมเดาว่า:

  • TS 7 เปลี่ยน architecture เป็น Go-based (port จาก JS)
  • --noRecursionLimits flag อาจจะต้อง patch ใหม่ทั้งหมด
  • หรือ TS 7 อาจจะมี feature ที่ทำให้ไม่ต้อง patch แล้ว

ถ้ามีอัพเดทจะมาเขียนเพิ่มครับ

สรุป​

TypeScript types ไม่ใช่แค่ "type สำหรับ IDE" — แต่เป็น ภาษา FP ที่ซ่อนอยู่ในอีกภาษา ที่มี functions, pattern matching, recursion, lists ครบ

ใช้ได้จริงในงาน dev ปกติ — type-safe URL parser, state machines, API validators, form types แต่ไม่ต้องไปถึงขั้น patch compiler (compile-time crimes) เพราะอยู่ในโซน niche

ส่วน TypeScript 7 — ยังไม่รู้ว่าจะ patch ง่ายหรือยากกว่าเดิม เพราะ architecture เปลี่ยนเป็น Go-based รอ Hugo อัพเดทก่อน

อ้างอิง​

แชร์บทความ
☕

เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว

Buy Me a Coffee
Loading...