Functional Programming ที่ซ่อนอยู่ใน TypeScript Types — เรียน FP ผ่าน type system ไม่ต้องเขียน Haskell
สารบัญ
- TL;DR
- Type System = Functional Language ที่ซ่อนอยู่
- Toolkit ที่ Hugo แนะนำ (primer)
- 1. Type functions = Generic Types
- 2. Pattern matching พื้นฐาน
- 3.
infer— จับ pattern - 4. Default types ทำให้ signature สะอาด
- 5. Union distribution — ตัวที่หลายคนไม่รู้
- 6. Recursion + accumulator
- ตัวอย่างจริง: HackerRank "Sum of Odd Numbers" ใน Typesystem
- Input
- Step 1: Peano numbers — represent natural numbers
- Step 2: แปลง Peano ↔ Number
- Step 3: Add
- Step 4: IsOdd
- Step 5: Glue ทั้งหมดเข้าด้วยกัน
- Step 6: Recursion limit เจอที่ 100 elements
- Step 7: Patch compiler (ของจริง Hugo ทำ)
- Optimization จาก comments
- 1.
IsOddแบบ template literal (Merry-Lane) - 2.
BuildTuple+Addแบบ tuple-length (Merry-Lane) - 3.
KeepOddปัญหา — map ไม่ใช่ filter (canarydev) - 4. ทำไมไม่ patch TypeScript 7? (gbitten)
- ใช้จริงในงาน dev ปกติได้ไหม?
- 1. Parse query string / form data แบบ type-safe
- 2. Type-safe state machines
- 3. API response validators (compile-time)
- 4. Auto-generate form types จาก schema
- ทำไมถึงควรเรียนเรื่องนี้?
- แล้ว TypeScript 7 ล่ะ?
- สรุป
- อ้างอิง
บันทึก — 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 ทั่วไป |
|---|---|---|
| Function | type F<A> = ... | f :: A -> B |
| Pattern matching | T extends X ? Y : Z | case x of ... |
| Type variables | infer V | f :: forall a. a -> ... |
| Recursion | type R<T> = T extends ... ? R<...> : ... | recursion ปกติ |
| Lists | tuples [H, ...T] | [a] |
| Numbers | tuple length | Int |
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 เหตุผล:
- มันเป็น FP ที่ apply ได้จริง — ถ้าเขียน TypeScript เป็นหลัก คุณเล่น FP ทุกวันอยู่แล้ว แค่ไม่รู้ตัว
- Compile-time safety — error ที่ typesystem จับได้ = runtime error ที่หายไป
- ปูพื้นฐาน 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)
--noRecursionLimitsflag อาจจะต้อง 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 อัพเดทก่อน
อ้างอิง
- Functional programming with TS types only — r/typescript — โพสต์ต้นเรื่อง
- The Functional Language Hiding Inside TypeScript's Types — Hugo Vilela — primer เรื่อง toolkit (Part 0)
- I Patched the TypeScript Compiler to Add Up Nine Numbers — Hugo Vilela — Part 1 HackerRank
- TypeScript Handbook — Generics — official docs
- TypeScript Handbook — Conditional Types — official docs
- UnboundedTypeScript — GitHub — patched compiler ของ Hugo
- TypescriptCompetitiveProgramming — GitHub — repo ตัวอย่าง
- Typing the Technical Interview — lgastako — แนะนำโดย commenter
- ts-math — PhilippDehler — type-level math library
- type-fest — sindresorhus — utility types collection
- Doom in TypeScript types — YouTube — "less mad than this" ตามที่ Arve comment
เนื้อหานี้มีประโยชน์ไหม? ช่วยสนับสนุนค่ากาแฟให้ผู้เขียนสักแก้ว
Buy Me a Coffee