Skip to content
CodeSook
CodeSook

Power up your typescript project with Effect (Beginner)


25 cli tools logo
#Effect#Typescript#FunctionalProgramming
CodeSookPublish: 18th January 2025

สวัสดีครับเพื่อนๆ
กลางเดือนที่ผ่านมานี้ผมจัด workshop ทำ Backend ด้วย Hono+Effect คนเต็มไปแล้ว เพราะพื้นที่เรามีจำกัด

บทความนี้เป็นส่วนหนึ่งของเนื้อหาที่สอนใน Workshop ด้วย
จะเนื้อหาจะไม่หนักมาก พอให้เห็นภาพว่า Effect มันใช้ทำอะไร มีประโยชน์กับเรายังไง ข้อดี ข้อเสีย

Blog นี้จะยาวหน่อยนะครับ ผมพยายามที่จะยกตัวอย่างให้เห็นภาพมากที่สุด เท่าที่ผมจะคิดออก โดยอิงจากประสบการณ์อันน้อยนิดของผมเอง 😆😆 ใครไม่อยากเสียเวลากระโดดลงไปอ่านด้านล่างได้เลย นะ


What is Effect

Effect เป็น library ตัวหนึ่งในโลกของ Typescript สร้างมาเพื่อแก้ปัญหาของ Typescript อีกทีนึง
Effect วางจุดยืนของตัวเองเป็น Meta-framework ของ Typescript

เนื่องจากว่า Typescript เป็น Superset ของ Javascript ปัญหาที่อยู่ในโลกของ Javascript บางส่วนก็ยังมีอยู่ในโลกของ Typescript ด้วยเหมือนกัน
เช่น การจัดการ Error ด้วย try-catch block แล้วแยกไม่ออกว่า error เกิดขึ้นตรงไหน หรือการที่ javascript ไม่ได้มี standard library ตรงนี้ Effect ก็สร้าง Modules ต่างๆมา support ส่วนที่หายไปนี้ พร้อมทั้งเพิ่ม API, Method, Utilities ต่างๆมาอีก ทำให้เราเอา functions มาร้อยเรียงกันได้ง่ายๆด้วย (Composable API) ซึ่งจริงๆ Deno Team ก็พยายามแก้ด้วยการสร้าง standard library ของตัวเอง แล้วมาเอามาใช้เป็น standard library ของ Typescript ด้วยเช่นกัน แต่ก็เป็นคนละตัวกันนะ


ข้อดีของ Effect

  1. Type-safety: สุดๆ มี Option, Either, Cause, Effect ทำให้เกิด type-safety ในการเขียน code สุดๆ คือเวลาเราเรียกใช้งาน function ตัว function นั้นจะบอกเราชัดเจนเลยว่าจะได้ return อะไรกลับมา แล้วถ้า Error จะได้ Error อะไร แล้วถ้าเราเอา functions หลายๆตัวมาเรียกใช้งานต่อๆกัน Effect ก็จะบอกเราได้ทั้งหมดว่า สุดท้ายเราจะได้ return อะไร แล้วจะเกิด Error อะไรขึ้นได้บ้าง ใช้แทน fp-ts, ramda, purify-ts ได้เลย
  2. Monadic approach: Effect มี Monad ที่ชื่อว่า Effect ซึ่งจะบอกเราชัดเจนเลยว่า code return อะไร, มี Error อะไบ้าง, มี Dependencies อะไรบ้างที่ต้องใส่เข้ามาก่อนถึงจะรันโค้ดนี้ได้ ทำให้เราเอา Effect ไปรันต่อๆกันเป็นทอดๆได้ โดยไม่ต้องสนใจ Error เลย ทำให้เรา focus แค่ที่ business logic ของเราเพียงอย่างเดียว
  3. Forget the Asynchronous let effect do it for you: เวลาเราเขียน Typescript function มันจะแยกกันชัดเจนไปเลยว่า function ที่เราสร้างต้องการให้เป็น Asynchronous ด้วย keyword async เวลาเราใช้ Effect ก็ไม่ต้องไปสนใจตรงนี้แล้ว เขียนแบบ Synchronous ไปได้เลย เดี๋ยว Effect runtime จัดการให้เอง เวลาเอา functions หลายๆตัวมาทำงานต่อกัน code เราจะอ่านได้ง่ายมากๆ
  4. Retry machanism: ถ้าหากว่า Effect เกิด Fail เราสามารถให้ Effect ทำงานใหม่อีกรอบ อีกกี่รอบก็กำหนดได้ โดยเว้นช่วงเวลาห่างกันเท่าไร ก่อนจะทำ retry ครั้งถัดไปก็กำหนดได้ ตัวอย่างเช่น มี Effect ที่ทำการดึง data จาก api ผ่าน fetch api แต่ว่าได้ error 500 Internal server error เราก็ให้ Effect ทำ retry เฉพาะส่วนนี้ได้ ไม่กระทบส่วนอื่นๆ ซึ่งการจะทำอะไรแบบนี้ได้ มันไม่ได้ง่ายเลยถ้าเราไม่มีตัวช่วย
  5. pipe function(แทน pipe operator): Effect มี pipe function มาให้ใช้ ตรงนี้ผมชอบมาก เราสามารถเอา return value จาก function ก่อนหน้าส่งมาให้เป็น argument ใน function ถัดไปได้เลย
  6. มี Immutable data structure Array, HashMap, HashSet, Trie etc etc พร้อม Composable API ที่ใช้งานง่ายมากขึ้น ถ้าใครเคยใช้ lib immutable, lodash ก็ใช้แทนได้เลย
  7. Resource safety: อย่างที่บอกไปว่า Effect บอกว่า Return อะไร Error อะไร และ ต้องการ Dependencies อะไร ส่วนหลังนี้แหละ การบอกว่าต้องใส่ Dependencies อะไรบ้างก่อนที่จะรันโค้ดได้ ทำให้เราทำ Dependencies injection ได้ง่ายๆ ทำให้ code เรา test ได้ง่ายไปอีก
  8. Rich ecosystem: อย่างที่บอกว่า Effect มี modules มาให้เยอะมากๆ เช่น DateTime ใช้แทน Date object ที่เวลาเราใช้งานมักจะเจอกับความประหลาดของมัน ใช้แทน date-fns, date-fns-tz หรือ moment ได้เลยนะสำหรับ DateTime
  9. Observability: Effect มี built-in tracing มาให้ในตัว ไม่ต้องเขียน tracer.startActiveSpan() ครอบแล้ว ดีเยี่ยมสุดๆ อันนี้มีใน workshop ด้วยนะ
  10. Runtime validation: Effect มี module อยู่ตัวนึงที่ชื่อว่า Effect Schema เป็น runtime validator ตัวนึงเหมือน Zod น่ะแหละ แต่ของ Effect มีความสามารถมากกว่า เช่น
    • Effect Schema จะเป็น Immutable data โดย default เลย
  11. Pattern Matching: สามารถทำ pattern matching ได้ด้วย สำหรับใครที่เขียน Javascript และ Typescript อาจจะไม่คุ้นเคยกับ Pattern matching ก็ไม่เป็นไรค่อยๆศึกษาเพิ่มเติม ผมจะบอกว่ามัน powerful มากๆ และ type-safety ด้วย สามารถใช้แทน ts-pattern หรือ unionize ได้เลย ผมว่าดีกว่าด้วย
    • แปลง data กลับไปกลับมาระหว่าง Typescript type กับ Schema type แบบปกติ
    • built-in branded-type อันนี้ใน workshop เราก็สอน
    • generate Json schema ได้เลยจาก Effect Schema มี built-in function มาให้แล้ว ตรงนี้สามารถเอาไปสร้าง OpenAPI Document ได้เลยด้วย
  12. Platform API: อันนี้ผมก็ชอบมากๆ ถ้าใครเขียน Nodejs แล้วสลับไปเขียน Bun น่าจะเคยเจอว่างานเดียวกันแต่ function ที่จะเรียกใช้งานไม่เหมือนกันเช่น ถ้าอยากอ่านไฟล์บน Node จะใช้ write function จาก node:fs ส่วนบน Bun จะใช้ write funciton จาก Bun Global module (Bun ก็ import node:fs ได้แหละ) จะเห็นว่าเขียน Typescript เหมือนกัน แต่ถ้าจะใช้งานบางอย่างมันดันเรียกใช้ function ไม่เหมือนกัน ทำให้ code เราถูก lock ผูกติดกับ Runtime ตัวนั้นๆไปเลย ถ้าอยากจะเปลี่ยน runtime ก็ต้องมาไล่แก้ function เหล่านั้นอีก Effect ช่วยเราแก้ปัญหานี้ด้วย Platform API นี่แหละ ทีมงาน Effect ก็เขียน standard function มาครอบ function ที่ในแต่ละ runtime มันเรียกใช้ต่างกันอีกชั้นนึง ทำให้เวลาเราใช้ function เหล่านั้นจาก Effect เราก็ไม่ต้องสนใจว่าจะใช้บน Runtime อะไร เดี๋ยว Effect จัดการให้ได้ (Platform API ยังไม่ stable ณ ตอนที่เขียนบทความนี้นะ)
  13. Concurrency API: Effect ได้เตรียม api มาให้เราใช้งานแบบ Concurrency ได้ง่ายๆเลย กำหนดได้ว่าจะให้ทำ Concurrency พร้อมๆกันครั้งละกี่ตัว ใส่ตัวเลขได้เลย หรือจะใส่แบบ unbounded เลยก็ได้, สามารถทำ Racing Concurrency ได้ด้วย เช่นพยายามทำงานหลายๆงานพร้อมๆกัน แต่ว่าเราต้องการงานที่เสร็จเป็นงานแรก Effect ก็จะหยุดการทำงานที่เหลือทั้งหมดให้เอง แล้วส่งผลลัพธ์จากงานที่เสร็จเป็นงานแรกมาให้เรา
  14. Easy to Construct Config(ENV): ผมชอบการเขียน config schema ของ Effect พอสมควร มันสามารถแยก Nested config ออกไปเป็นอีก object ได้ด้วย เช่นเรามี env HOST_IP, HOST_PORT, DATABASE_URL, DATABASE_PORT มันจะสร้าง object ให้เราเอาไปใช้ง่ายๆแบบนี้
const env: {
host: {
ip: string
port: number
},
database: {
url: string
port: number
}
}
  1. ต่อเนื่องจากข้อบน บางครั้ง data ที่วิ่งในระบบของเราเป็น sensitive data เช่น .env มี DATABASE_PASSWORD ด้วย เวลาเรา log env ทั้งหมดออกมาดู เราสามารถให้ Effect แสดง password เป็นตัวอักษร ******* ได้ด้วย แสดงถึงการมีอยู่ของ DATABASE_PASSWORD แต่ที่ log ออกมาจะไม่รู้ว่าคืออะไร รู้แค่ว่ามันมีอยู่

ตัวอย่างภาพ Tracing ที่ได้

tracing example

จะเห็นว่ามัน Powerful มากๆ

ข้อเสียของ Effect

  1. Learning curve: ผมว่านี่เป็นกำแพงขนาดใหญ่เลยที่ทำให้ Effect ไม่ได้รับความนิยมเท่าที่ควร มันมี API มากมายให้เราใช้งาน อย่างที่ผมบอกว่ามี API ให้เลือกใช้เยอะมากๆ ทำให้ code เราอ่านง่ายมากๆ แต่เราต้องศึกษาเพิ่มอีกเยอะเลยว่า API มันทำอะไร มัน compose กันได้อย่างไร เนื่องจากว่า Effect เน้นไปที่การ compose function ต่างๆ ไม่ได้ใช้ method chaining แล้วมือใหม่เวลาเขียนก็จะใช้วิธีการใส่ . ดูว่า autocomplete มีอะไรให้ใช้บ้างนะ แล้วก็ตาแตกเพราะมันมีเยอะเหลือเกิน 😭
  2. Large Codebase: Effect เป็น single library ทุกๆ Modules อยู่ใน lib เดียวเลย ทำให้ขนาดมันใหญ่มากๆ แต่ทั้งนี้ Effect ใช้วิธีการเขียนที่เรียกว่า Tree-shakable แบบ 100% นั่นหมายความว่า code ส่วนไหนที่เราไม่ได้ใช้ มันก็จะไม่ได้ถูกรวมเข้าไปตอนที่เรา build app ทำให้ app เราไม่ได้ใหญ่ตามขนาดของ Effect นั่นเอง
  3. Easy Examples in the docs: ใน document ของ Effect มีการยกตัวอย่าง แต่มันเป็นตัวอย่างง่ายๆ บางครั้งเราก็ไม่เห็นภาพว่าจะเอาไปใช้ในงานจริงๆได้อย่างไร
  4. Few Examples: เนื่องจากข้อ 1. Learning curve มันสูง และ ข้อ 3. Easy Examples ทำให้ตัวอย่างที่มีที่ให้เราได้ไปศึกษาก็น้อยลงตามไปด้วย พวก blogs, Tutorials ต่างๆ ก็ยังมีน้อยอยู่ ต้องไปตามอ่านพวก Github project ที่เขาใช้ Effect ซึ่งก็จะหายากอีกเช่นกัน หลายคนคงไม่ชอบที่จะไปตามอ่านสักเท่าไร เพราะไม่รู้จะเริ่มต้นตรงไหน ส่วนตัวผมก็ใช้วิธีไปตามอ่านใน Github นี่เช่นกัน ถ้าโชคดีเขาเขีน Test ผมก็จะเริ่มที่อ่าน Test ก่อนเลย ถ้าไม่มีก็ต้องงมๆกันไป ซึ่งมันจะเสียเวลาแหละ ด้วยเหตุนี้หลายคนที่ไม่มีเวลาก็เลยยังไม่พร้อมที่จะศึกษา เลยส่งผลให้มีตัวอย่างน้อย

Show me the code

ตัวอย่างที่จะเขียนให้ดูนี้เป็นแค่ส่วนหนึ่งที่ Effect ทำได้นะครับ และเป็นส่วนที่ผมคิดว่ามันคือจุดเด่นของ Effect เลย นั่นก็คือ Error handling Effect make it impossible to forget error handling

Effect ทำให้เราสามารถ focus กับ flow การทำงานของ Code เฉพาะส่วนที่เป็น Happy case ได้เลย แล้วผลักการ handle error ไปให้กับเพื่อนที่เอา function/program ไปเรียกใช้งานต่อ โดย Effect จะเป็นคนบอกเพื่อนเราทั้งหมดว่า ถ้าใช้งาน function นี้เพื่อนเราอาจจะเจอกับ Errors อะไรได้บ้าง type-safety สุดๆ 🛡️😊

ตัว Effect เองมี Modules เยอะแยะ ทำได้มากกว่าตัวอย่างนี้อีกเยอะเลยละ

ผมจะใช้การเขียนแบบ functional นะครับ ใครไม่คุ้นก็ไม่เป็นไร ค่อยๆดูกันไป แต่ว่าผมจะใส่วิธีที่ทุกคนน่าจะคุ้น เป็นการเขียแบบ imparative ให้ในตอนท้ายนะครับ

Before start Let’s play game

ก่อนจะเริ่มผมมีเกมมาให้เล่น ถ้าใครไม่ได้เข้า workshop ก็ให้ลองคิดตามดูก่อนครับ

วิธีการเล่นเป็นแบบนี้ครับ

game1

มีกล่องของขวัญอยู่ 1 กล่อง 🎁 สามารถใส่ของลงไปได้ 1 อย่าง เลือกได้แค่

  1. ใส่ของขวัญสุดแฮปปี้ 🎁 จะเป็นอะไรก็ได้ที่คนเปิดแฮปปี้ 😊 จะใส่ได้แค่ชิ้นเดียวนะ
  2. ใส่ระเบิด 💣 จะใส่มากน้อยแค่ไหนก็ได้ ระเบิดจะมี 7 สี
  • ม่วง
  • แดง
  • เขียว
  • น้ำเงิน
  • ส้ม
  • เหลือง
  • ชมพู
game2

กล่องของขวัญนี้จะถูกส่งต่อๆกันไปได้ เมื่อผู้เล่นได้รับกล่องของขวัญสามารถทำ action ได้ 1 ใน 2 อย่างดังนี้

  1. เลือกเปิดของขวัญเลย โดย

    • ถ้ามีของขวัญ ไม่มีระเบิด ก็รับของขวัญไปได้เลย จบเกมถือว่าผู้เล่นชนะไปเลย
    • ถ้าเจอระเบิดอย่างน้อย 1 ลูกจะต้องไปทำ action ก่อน เลือกทำได้ 1 อย่าง ใน 2 อย่างนี้
      1. เลือกปลดระเบิด ผู้เล่นต้องเดาว่ามีระเบิดสีอะไรบ้าง แบบให้ตรงเป๊ะๆ ไม่มากไม่น้อย ถ้าเดาถูกทั้งหมดแบบเป๊ะๆ (เอาแค่สีตรงกันก็พอนะไม่สนใจจำนวนระเบิด) จะถือว่าชนะ ไปเลย
        แต่ถ้าผิดแม้แต่สีเดียวจะถือว่าแพ้ จบเกมเลย ส่วนคนชนะจะเป็นคนที่ส่งกล่องมาให้เรา
      2. เลือกไม่ปลดระเบิด และจะส่งกล่องต่อให้คนด้านหน้า โดยสามารถ
        • เปลี่ยนของขวัญเป็นอย่างอื่นได้ ให้เอาของขวัญอันเก่าออก แล้วใส่อันใหม่เข้าไป ได้แค่ 1 อย่างเท่านั้น
        • ใส่ระเบิดเพิ่มได้ กี่ลูกก็ได้
  2. เลือกไม่เปิด แต่ขอแอบดูของด้านใน ถ้าผู้เล่นเลือกข้อนี้จะไม่ถือว่าชนะ และก็จะไม่แพ้ด้วย แต่ยังมีโอกาสชนะถ้าคนถัดไปเปิดเจอระเบิด พอแอบดูเสร็จแล้วก็ส่งให้กับคนถัดไป

วิธีการเล่นคือ
มีกล่อง 2 กล่อง 1 กล่องใหญ่ 1 กล่องเล็ก กล่องเล็กสามารถใส่ลงกล่องใหญ่ได้ก็จะดี
มีของขวัญ 3 อย่าง ในที่นี้คงเป็นแบงค์เทา แบงค์ม่วง และแบงค์เขียว ให้ผู้เล่นต่อแถวกัน ห้ามผู้เล่นคุยกัน ผู้เล่นคนแรกจะเป็นคนหลังสุดซึ่งจะเป็น Modulator จะเอาของขวัญใส่ลงไปในกล่องใหญ่และเอาระเบิดใส่กล่องเล็ก
จากนั้นส่งกล่องนี้ไปให้กับเพื่อนคนที่อยู่ด้านหน้า คนที่ได้รับกล่องก็เลือก action ว่าจะเปิดดูของขวัญหรือเปล่า หรือจะแค่แอบดู
ถ้าเลือกเปิดก็จะเห็นเลยว่าของขวัญคืออะไร แต่ห้ามเปิดกล่องเล็กที่มีระเบิดนะ
แล้วก็เลือกว่าจะปลดระเบิดไหม ถ้าเลือกปลดก็เดาว่ามีระเบิดสีอะไรบ้าง ถ้าไม่ปลดก็เปลี่ยนของขวัญได้ เราก็แค่คิดว่าอยากให้เพื่อนได้อะไร หรือถ้าเพื่อนพลาดแล้วเราจะได้อะไร
แล้วก็ส่งไปให้คนด้านหน้าไปเรื่อยๆ
พอถึงผู้เล่นคนสุดท้าย คนนี้ไม่มีิสิทธิ์เลือกต้องเปิดกล่องอย่างเดียวเท่านั้น ถ้าเจอของขวัญก็ชนะไปเลยยย
แต่เนื่องจากว่าเป็นคนสุดท้าย เราจะให้ใช้สิทธิ์ เปิดโล่ ถ้าเปิดมาแล้ว เจอระเบิด แล้วปลดระเบิดไม่สำเร็จ ก็จะไม่ตาย สามารถขอเล่นรอบถัดไปได้ แต่ถ้าปลดสำเร็จจะได้ของรางวัลแค่ครึ่งเดียว

เมื่อชนะก็เอาของรางวัลไปได้เลย และหมดสิทธิ์เล่นในเกมต่อไป
ถ้าแพ้ก็จะไม่ได้อะไรและหมดสิทธิ์ในเกมต่อไปเช่นกัน
ถ้าแพ้แต่มีโล่ สามารถเช่นเกมต่อไปได้
ผู้เล่นที่เหลือด้านหน้าจะอดเล่นเมื่อผู้เล่นปลดระเบิดพลาด ยังมีสิทธิ์ได้เลยในเกมถัดไป
ก็เริ่มเกมใหม่ได้เลย
แต่ผู้เล่นด้านหลังจะถือว่าเป็นผู้ชนะ ถึงแม้ว่าเขาจะไม่ได้เป็นคนใส่ระเบิดก็ตาม เอาของขวัญไปได้เลย

สามารถเล่นได้หลายรอบจนกว่าจะพอใจได้ของขวัญกันทุกคน

ใน workshop เราจะเล่นกัน 3 รอบ

มาดูตัวอย่างกันเลยดีกว่า

โจทย์

ผมจะใช้การ flow การ Registration ในตัวอย่างนี้นะครับ โดยเราเป็นคนเขียน flow การ register ที่ฝั่ง Backend
เมื่อได้รับ request มาจาก frontend หรือจากไหนก็แล้วแต่ งานของเราคือต้องไปสร้าง user ใหม่ใน Database แล้วส่ง email ยืนยันไปที่ email ของผู้สมัครผ่าน third party email provider ที่ชื่อ Resend
แต่ในตัวอย่างนี้เราจะไม่ได้ส่งจริงนะ สร้าง function ของ Resend หลอกๆไว้เฉยๆ

Traditional without Effect

มาเริ่มที่สร้าง type กันก่อน user ของเราจะมี type แบบนี้

register.ts
type
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
= {
id: string
id
: string
email: string
email
: string
hashedPassword: string
hashedPassword
: string
}
type
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: string
password: string
password
: string
}

และมี functions ที่เราจะ import มาจากไฟล์อื่นๆ แต่ในที่นี้ขอ ละ ไว้
ผมก็จะเขียนสั้นๆโดยใช้ declare ไว้ด้านหน้า เพราะว่า function เหล่านี้ไม่ได้อยู่ใน scope ที่ผมต้องการจะยกตัวอย่าง เอาเป็นว่ามันมาจากที่อื่น ซึ่งเราไม่ได้เขียนเอง มีคนทำไว้แล้ว เราไม่สามารถเข้าไปแก้ไข functions เหล่านั้นได้ เรามีหน้าที่แค่เรียกใช้งาน และมันก็มี signature ตามนี้

register.ts
// assume hashPassword function from lib like `bcrypt` or `argon2`
declare function
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string>
// assume isEmailExist function from our another module that query the User table
declare function
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null>
// assume saveUser function is in our another module that save use into the database
declare function
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>

แล้วเราจะสร้าง function ที่เอาไว้เช็คว่า email ที่ใส่เข้ามามันเป็น email จริงๆ ไม่ใช่ว่าจะเป็น text อะไรก็ได้ ด้วย function นี้

function
function validateEmail(email: string): boolean
validateEmail
(
email: string
email
: string): boolean {
const
const emailRegex: RegExp
emailRegex
= /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
return
const emailRegex: RegExp
emailRegex
.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
email: string
email
)
}

มี functions ที่เอาไว้ตรวจสอบ password ด้วย 2 functions

function
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
password: string
password
: string): boolean {
return
password: string
password
.
String.length: number

Returns the length of a String object.

length
>= 8
}
function
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
password: string
password
: string): boolean {
return /[^a-z0-9]/i.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
password: string
password
)
}

แล้วเราก็จะเอาทุกๆ function มารวมกันกลายเป็น functio register() แบบนี้

async function
function register(user: UserInput): Promise<User>
register
(
user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
> {
const
const isValidEmail: boolean
isValidEmail
=
function validateEmail(email: string): boolean
validateEmail
(
user: UserInput
user
.
email: string
email
)
if (
const isValidEmail: boolean
isValidEmail
=== false) {
throw new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("email is invalid")
}
const
const isPwdLongerThan8: boolean
isPwdLongerThan8
=
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
user: UserInput
user
.
password: string
password
)
if (
const isPwdLongerThan8: boolean
isPwdLongerThan8
=== false) {
throw new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("password need to be 8 characters long")
}
const
const isContainsSpecialChar: boolean
isContainsSpecialChar
=
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
user: UserInput
user
.
password: string
password
)
if (
const isContainsSpecialChar: boolean
isContainsSpecialChar
=== false) {
throw new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("password need to be contains at least one special character")
}
const
const hashedPassword: string
hashedPassword
= await
function hashPassword(password: string): Promise<string>
hashPassword
(
user: UserInput
user
.
password: string
password
)
const
const existUser: User | null
existUser
= await
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
user: UserInput
user
.
email: string
email
)
if (
const existUser: User | null
existUser
!== null) {
throw new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("email already exist")
}
const
const newUser: User
newUser
= await
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
({
email: string
email
:
user: UserInput
user
.
email: string
email
,
hashedPassword: string
hashedPassword
,
})
return
const newUser: User
newUser
}

register() ก็เอา functions ต่างๆที่เราสร้างไว้มาเรียกใช้งาน เพื่อทำให้ได้ระบบ register ตามที่ requirement ขอมา

เนื่องจากว่าอาจจะเกิด Error ได้
เราก็จะเอาไปเรียกใช้ใน try-catch block เพราะว่าอาจจะเกิด Error ขึ้นได้ app เราจะได้ไม่ระเบิดไปซะก่อน

const
const user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: "[email protected]",
password: string
password
: "test",
}
async function
function registerController(): Promise<Response | undefined>
registerController
() {
try {
const newUser = await register(
const user: UserInput
user
)
const newUser: User
function register(user: UserInput): Promise<User>
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
const newUser: User
newUser
))
}
catch (error) {
function (local var) error: unknown
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.error(message?: any, ...optionalParams: any[]): void

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

@sincev0.1.100

error
(
function (local var) error: unknown
error
)
}
}

เมื่อดูที่ function register() เราจะเห็นว่ามันจะ return Promise<User> แต่ไม่ได้บอกว่าอาจจะเกิด Error นะ เราก็เลยต้องใช้ try-catch block เข้ามาดักจับ Error ที่อาจจะเกิดขึ้น
ทีนี้เมื่อเราดูต่อที่ type ของ ตัวแปร error ใน catch block จะเห็นว่า type มันคือ unknown

แล้วเราจะรู้ได้ยังไงละว่า Error ที่จะเกิดขึ้นมีอะไรบ้าง
แล้วถ้าเราไม่ใช่คนที่เขียน function นี้ เพื่อนเราเป็นคนทำ เราจะต้องไปถามเขาใช่ไหม เพื่อนก็อาจจะจำไม่ได้ว่า Error จะเป็นอะไรได้บ้าง สุดท้ายเราก็ต้องไปไล่ดูโค้ดทั้งหมดอยู่ดี
ถ้าแค่ในตัวอย่างนี้มันก็พอจะไล่ได้แหละ แต่ในงานจริงๆ codebase ใหญ่กว่านี้มาก มันจะเสียเวลามากๆเลยที่ต้องมานั่งไล่
ย่ิงแล้วไปใหญ่ถ้า function นี้มาจาก library ซึ่งถ้าโชคดีเขาอาจจะมี document บอกว่า Error เป็นอะไรได้บ้าง
แต่ถ้าโชคไม่ดีเขาไม่มีให้ละ เราก็ต้องจำกัดวงระเบิดไม่ให้มันกระจาย ด้วยการเขียน function Wrapper มาครอบมันไว้

Discriminated Unions

เอาละเพื่อนๆน่าจะพอเดาทางได้ละว่า Effect จะมาแก้ปัญหาตรงไหน
แต่ผมจะยังไม่พาไปใช้ Effect ลองดูทางออกอื่นๆก่อนว่าถ้าไม่ใช้ Effect จะทำยังไงดี
เพราะเดี๋ยวผมจะแสดงให้ดูว่า Effect ให้เราได้มากกว่านั้นอีก ทนอ่านต่อไปอีกนิดนะ มาดูกันต่อ

หนึ่งในวิธีนั้นก็จะเป็นวิธีที่ใช้ Discriminated Unions Type
หลักการก็คือเราจะให้ function ที่มันอาจจะเกิด Error ได้ ให้มัน return Error as a value แทนที่จะ throw new Error()
แล้วจะบังคับให้คนที่เอา function เหล่านี้ไปใช้งานต่อต้องจัดการ handle error ซะก่อน

มาทำให้ function ที่อาจจะเกิด Error ทำการ return Error as a value กัน

เรามาทำ Discriminated type กันก่อน 👇

type
type Ok<A> = {
type: "Ok";
value: A;
}
Ok
<
function (type parameter) A in type Ok<A>
A
> = {
type: "Ok"
type
: "Ok"
value: A
value
:
function (type parameter) A in type Ok<A>
A
}
type
type Err<E> = {
type: "Err";
error: E;
}
Err
<
function (type parameter) E in type Err<E>
E
> = {
type: "Err"
type
: "Err"
error: E
error
:
function (type parameter) E in type Err<E>
E
}
type
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
function (type parameter) A in type OkOrErr<A, E>
A
,
function (type parameter) E in type OkOrErr<A, E>
E
> =
type Ok<A> = {
type: "Ok";
value: A;
}
Ok
<
function (type parameter) A in type OkOrErr<A, E>
A
> |
type Err<E> = {
type: "Err";
error: E;
}
Err
<
function (type parameter) E in type OkOrErr<A, E>
E
>

ส่วน function ที่เป็นมาจาก lib อื่นๆ หรือ function อะไรก็แล้วแต่ที่เราคุมไม่ได้อะ ให้เราเขียน Wrapper มาครอบมันซะ ถ้ามันจะมีปัญหาก็ให้ปัญหามันอยู่แค่ในนั้น แบบนี้ 👇

// assume hashPassword function from lib like bcrypt or argon2
declare function
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string>
async function hashPasswordWrap(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<string,
interface Error
Error
>> {
function hashPasswordWrap(password: string): Promise<OkOrErr<string, Error>>
try {
const
const hashedPwd: string
hashedPwd
= await
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
)
return {
type: "Ok"
type
: "Ok",
value: string
value
:
const hashedPwd: string
hashedPwd
,
}
}
catch {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("password is invalid"),
type: "Err"
type
: "Err",
}
}
}
// assume isEmailExist function from our another module that query the User table
declare function
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null>
async function findOneByEmailWrap(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null,
interface Error
Error
>> {
function findOneByEmailWrap(email: string): Promise<OkOrErr<User | null, Error>>
try {
const
const user: User | null
user
= await
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
)
return {
type: "Ok"
type
: "Ok",
value: User | null
value
:
const user: User | null
user
,
}
}
catch {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("find one by email failed"),
type: "Err"
type
: "Err",
}
}
}
// assume saveUser function is in our another module that save use into the database
declare function
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>
async function saveUserWrap(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
,
interface Error
Error
>> {
function saveUserWrap(user: Omit<User, "id">): Promise<OkOrErr<User, Error>>
try {
const
const newUser: User
newUser
= await
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
)
return {
type: "Ok"
type
: "Ok",
value: User
value
:
const newUser: User
newUser
,
}
}
catch {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("save user failed"),
type: "Err"
type
: "Err",
}
}
}

ส่วนนี้ไม่ได้เปลี่ยนอะไร 👇👌

function
function validateEmail(email: string): boolean
validateEmail
(
email: string
email
: string): boolean {
const
const emailRegex: RegExp
emailRegex
= /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
return
const emailRegex: RegExp
emailRegex
.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
email: string
email
)
}
function
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
password: string
password
: string): boolean {
return
password: string
password
.
String.length: number

Returns the length of a String object.

length
>= 8
}
function
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
password: string
password
: string): boolean {
return /[^a-z0-9]/i.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
password: string
password
)
}

เราก็จะเอา functions ทั้งหลายมาเขียนเป็น register function แบบนี้

async function
function register(user: UserInput): Promise<OkOrErr<User, Error>>
register
(
user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
,
interface Error
Error
>> {
const
const isValidEmail: boolean
isValidEmail
=
function validateEmail(email: string): boolean
validateEmail
(
user: UserInput
user
.
email: string
email
)
if (
const isValidEmail: boolean
isValidEmail
=== false) {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("email is invalid"),
type: "Err"
type
: "Err",
}
}
const
const isPwdLongerThan8: boolean
isPwdLongerThan8
=
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
user: UserInput
user
.
password: string
password
)
if (
const isPwdLongerThan8: boolean
isPwdLongerThan8
=== false) {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("password need to be 8 characters long"),
type: "Err"
type
: "Err",
}
}
const
const isContainsSpecialChar: boolean
isContainsSpecialChar
=
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
user: UserInput
user
.
password: string
password
)
if (
const isContainsSpecialChar: boolean
isContainsSpecialChar
=== false) {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("password need to be contains at least one special character"),
type: "Err"
type
: "Err",
}
}
const
const hashedPasswordResult: OkOrErr<string, Error>
hashedPasswordResult
= await
function hashPasswordWrap(password: string): Promise<OkOrErr<string, Error>>
hashPasswordWrap
(
user: UserInput
user
.
password: string
password
)
if (
const hashedPasswordResult: OkOrErr<string, Error>
hashedPasswordResult
.
type: "Ok" | "Err"
type
=== "Err") {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("hashed Password error"),
type: "Err"
type
: "Err",
}
}
const
const existUserResult: OkOrErr<User | null, Error>
existUserResult
= await
function findOneByEmailWrap(email: string): Promise<OkOrErr<User | null, Error>>
findOneByEmailWrap
(
user: UserInput
user
.
email: string
email
)
if (
const existUserResult: OkOrErr<User | null, Error>
existUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("database error"),
type: "Err"
type
: "Err",
}
}
if (
const existUserResult: Ok<User | null>
existUserResult
!== null) {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("email already exist"),
type: "Err"
type
: "Err",
}
}
const
const newUserResult: OkOrErr<User, Error>
newUserResult
= await
function saveUserWrap(user: Omit<User, "id">): Promise<OkOrErr<User, Error>>
saveUserWrap
({
email: string
email
:
user: UserInput
user
.
email: string
email
,
hashedPassword: string
hashedPassword
:
const hashedPasswordResult: Ok<string>
hashedPasswordResult
.
value: string
value
,
})
if (
const newUserResult: OkOrErr<User, Error>
newUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
return {
error: Error
error
: new
var Error: ErrorConstructor
new (message?: string) => Error
Error
("database error"),
type: "Err"
type
: "Err",
}
}
return {
type: "Ok"
type
: "Ok",
value: User
value
:
const newUserResult: Ok<User>
newUserResult
.
value: User
value
,
}
}
const
const user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: "[email protected]",
password: string
password
: "test",
}
async function
function registerController(): Promise<Response | undefined>
registerController
() {
try {
const
const newUserResult: OkOrErr<User, Error>
newUserResult
= await
function register(user: UserInput): Promise<OkOrErr<User, Error>>
register
(
const user: UserInput
user
)
if (
const newUserResult: OkOrErr<User, Error>
newUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: Error
msg
:
const newUserResult: Err<Error>
newUserResult
.
error: Error
error
}), {
ResponseInit.status?: number
status
: 404 })
}
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
const newUserResult: Ok<User>
newUserResult
.
value: User
value
), {
ResponseInit.status?: number
status
: 200 })
}
catch (
function (local var) error: unknown
error
) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.error(message?: any, ...optionalParams: any[]): void

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

@sincev0.1.100

error
(
function (local var) error: unknown
error
)
}
}
function registerController(): Promise<Response | undefined>
registerController
()

จะเห็นว่า function register() เอา function อื่นๆมาใช้งาน จะถูกบังคับให้ต้องดู type ก่อนว่าเป็น Ok หรือ Err ซึ่งเราก็ต้องจัดการกับ Error ก่อน สุดท้ายถ้าทุกอย่างผ่านไปได้ด้วยดีทั้งหมด เราก็ค่อย return ผลลัพธ์จริงๆที่ function register() ต้องส่งกลับไป

จะเห็นว่ามันยาวมากๆ แต่ก็อ่านเข้าใจได้ไม่ยากใช่ไหมครับ

แต่ก็ยังมีข้อเสียอยู่คือ controller ที่เป็นคนเอา register() มาใช้งาน ก็ไม่ได้รู้อยู่ดีว่า Error ที่จะเกิดขึ้นมีอะไรได้บ้าง
controller ที่รับหน้าที่ส่ง Response กลับไปหา Client จะต้องตัดสินใจว่าจะใส่ Body กับ Status เป็นอะไร
แต่จาก code ด้านบนจะเห็นว่า ตรง Error เราใส่ status 404 ไป เพราะไม่รู้ว่า Error ที่เกิดขึ้นจะเป็นอะไร ก็เลยตัดสินใจไม่ได้ว่าควรจะใช้ Status อะไรดี

เราจะมาแก้ปัญหานี้กันก่อน โดยเป้าหมายคือ ให้ register() บอกให้ได้ว่าจะมี Error อะไรเกิดขึ้นได้บ้าง

Discriminated Unions with Custom Error

วิธีการคือ เราจะสร้าง Custom Error ขึ้นมาก่อนแบบนี้

class
class CustomError
CustomError
{
constructor(public readonly
CustomError.msg: string
msg
: string, public readonly
CustomError.error?: unknown
error
?: unknown) { }
}

แล้วทีนี้ก็เอาไปใช้กับทุกๆ function ที่มันเคย return Error แบบนี้ 👇

// assume hashPassword function from lib like bcrypt or argon2
declare function
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string>
class
class HashPasswordError
HashPasswordError
extends
class CustomError
CustomError
{ }
async function
function hashPasswordWrap(password: string): Promise<OkOrErr<string, HashPasswordError>>
hashPasswordWrap
(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<string,
class HashPasswordError
HashPasswordError
>> {
try {
const
const hashedPwd: string
hashedPwd
= await
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
)
return {
type: "Ok"
type
: "Ok",
value: string
value
:
const hashedPwd: string
hashedPwd
,
}
}
catch (
function (local var) err: unknown
err
) {
return {
error: HashPasswordError
error
: new
constructor HashPasswordError(msg: string, error?: unknown): HashPasswordError
HashPasswordError
("hash password failed",
function (local var) err: unknown
err
),
type: "Err"
type
: "Err",
}
}
}
// assume isEmailExist function from our another module that query the User table
declare function
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null>
class
class FindOneByEmailError
FindOneByEmailError
extends
class CustomError
CustomError
{ }
async function
function findOneByEmailWrap(email: string): Promise<OkOrErr<User | null, FindOneByEmailError>>
findOneByEmailWrap
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null,
class FindOneByEmailError
FindOneByEmailError
>> {
try {
const
const user: User | null
user
= await
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
)
return {
type: "Ok"
type
: "Ok",
value: User | null
value
:
const user: User | null
user
,
}
}
catch (
function (local var) err: unknown
err
) {
return {
error: FindOneByEmailError
error
: new
constructor FindOneByEmailError(msg: string, error?: unknown): FindOneByEmailError
FindOneByEmailError
("find one by email failed",
function (local var) err: unknown
err
),
type: "Err"
type
: "Err",
}
}
}
// assume saveUser function is in our another module that save use into the database
declare function
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>
class
class SaveUserError
SaveUserError
extends
class CustomError
CustomError
{ }
async function
function saveUserWrap(user: Omit<User, "id">): Promise<OkOrErr<User, SaveUserError>>
saveUserWrap
(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
,
class SaveUserError
SaveUserError
>> {
try {
const
const newUser: User
newUser
= await
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
)
return {
type: "Ok"
type
: "Ok",
value: User
value
:
const newUser: User
newUser
,
}
}
catch (
function (local var) error: unknown
error
) {
return {
error: SaveUserError
error
: new
constructor SaveUserError(msg: string, error?: unknown): SaveUserError
SaveUserError
("save user failed",
function (local var) error: unknown
error
),
type: "Err"
type
: "Err",
}
}
}

ที่ function register() ก็จะเขียนให้ return error class ให้เป็นแบบนี้ 👇

class
class PasswordInvalid
PasswordInvalid
extends
class CustomError
CustomError
{ }
class
class UserAlreadyExist
UserAlreadyExist
extends
class CustomError
CustomError
{ }
async function
function register(user: UserInput): Promise<OkOrErr<User, PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>>
register
(
user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
,
class PasswordInvalid
PasswordInvalid
|
class HashPasswordError
HashPasswordError
|
class UserAlreadyExist
UserAlreadyExist
|
class SaveUserError
SaveUserError
|
class FindOneByEmailError
FindOneByEmailError
>> {
const
const isValidEmail: boolean
isValidEmail
=
function validateEmail(email: string): boolean
validateEmail
(
user: UserInput
user
.
email: string
email
)
if (
const isValidEmail: boolean
isValidEmail
=== false) {
return {
error: PasswordInvalid | UserAlreadyExist | HashPasswordError | SaveUserError | FindOneByEmailError
error
: new
constructor PasswordInvalid(msg: string, error?: unknown): PasswordInvalid
PasswordInvalid
("email is invalid"),
type: "Err"
type
: "Err",
}
}
const
const isPwdLongerThan8: boolean
isPwdLongerThan8
=
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
user: UserInput
user
.
password: string
password
)
if (
const isPwdLongerThan8: boolean
isPwdLongerThan8
=== false) {
return {
error: PasswordInvalid | UserAlreadyExist | HashPasswordError | SaveUserError | FindOneByEmailError
error
: new
constructor PasswordInvalid(msg: string, error?: unknown): PasswordInvalid
PasswordInvalid
("password need to be 8 characters long"),
type: "Err"
type
: "Err",
}
}
const
const isContainsSpecialChar: boolean
isContainsSpecialChar
=
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
user: UserInput
user
.
password: string
password
)
if (
const isContainsSpecialChar: boolean
isContainsSpecialChar
=== false) {
return {
error: PasswordInvalid | UserAlreadyExist | HashPasswordError | SaveUserError | FindOneByEmailError
error
: new
constructor PasswordInvalid(msg: string, error?: unknown): PasswordInvalid
PasswordInvalid
("password need to be contains at least one special character"),
type: "Err"
type
: "Err",
}
}
const
const hashedPasswordResult: OkOrErr<string, HashPasswordError>
hashedPasswordResult
= await
function hashPasswordWrap(password: string): Promise<OkOrErr<string, HashPasswordError>>
hashPasswordWrap
(
user: UserInput
user
.
password: string
password
)
if (
const hashedPasswordResult: OkOrErr<string, HashPasswordError>
hashedPasswordResult
.
type: "Ok" | "Err"
type
=== "Err") {
return {
error: PasswordInvalid | UserAlreadyExist | HashPasswordError | SaveUserError | FindOneByEmailError
error
:
const hashedPasswordResult: Err<HashPasswordError>
hashedPasswordResult
.
error: HashPasswordError
error
,
type: "Err"
type
: "Err",
}
}
const
const existUserResult: OkOrErr<User | null, FindOneByEmailError>
existUserResult
= await
function findOneByEmailWrap(email: string): Promise<OkOrErr<User | null, FindOneByEmailError>>
findOneByEmailWrap
(
user: UserInput
user
.
email: string
email
)
if (
const existUserResult: OkOrErr<User | null, FindOneByEmailError>
existUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
return
const existUserResult: Err<FindOneByEmailError>
existUserResult
}
if (
const existUserResult: Ok<User | null>
existUserResult
!== null) {
return {
error: PasswordInvalid | UserAlreadyExist | HashPasswordError | SaveUserError | FindOneByEmailError
error
: new
constructor UserAlreadyExist(msg: string, error?: unknown): UserAlreadyExist
UserAlreadyExist
("email already exist"),
type: "Err"
type
: "Err",
}
}
const
const newUserResult: OkOrErr<User, SaveUserError>
newUserResult
= await
function saveUserWrap(user: Omit<User, "id">): Promise<OkOrErr<User, SaveUserError>>
saveUserWrap
({
email: string
email
:
user: UserInput
user
.
email: string
email
,
hashedPassword: string
hashedPassword
:
const hashedPasswordResult: Ok<string>
hashedPasswordResult
.
value: string
value
,
})
if (
const newUserResult: OkOrErr<User, SaveUserError>
newUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
return
const newUserResult: Err<SaveUserError>
newUserResult
}
return {
type: "Ok"
type
: "Ok" as
type const = "Ok"
const
,
value: User
value
:
const newUserResult: Ok<User>
newUserResult
.
value: User
value
,
}
}

จะเห็นว่าทุกจุดที่เราเอา function ที่อาจจะมี Error มาใช้งาน เราจะต้องเช็คก่อนว่ามันมี type เป็น Err หรือเปล่า ถ้าใช่ Error นั้นคืออะไร
สุดท้ายถ้าผ่านทุกอย่างมาได้ก็ return ผลลัพธ์ที่ต้องการกลับไป
ที่ register() มี singature บอกด้วยว่า return type คือะไร ทำให้คนที่นำไปใช้งานรู้ได้เลยว่าอาจจะมี Error อะไรเกิดขึ้นบ้าง

เอา register() มาใช้ใน controller() กันต่อ

const
const user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: "[email protected]",
password: string
password
: "test",
}
async function
function registerController(): Promise<Response>
registerController
() {
try {
const
const newUserResult: OkOrErr<User, PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
= await register(
const user: UserInput
user
)
function register(user: UserInput): Promise<OkOrErr<User, PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>>
if (
const newUserResult: OkOrErr<User, PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
type: "Ok" | "Err"
type
=== "Err") {
const
const err: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
err
=
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
if (
const err: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
err
instanceof
class PasswordInvalid
PasswordInvalid
) {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 400 }) // Bad Request
}
if (
const err: HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
err
instanceof
class UserAlreadyExist
UserAlreadyExist
) {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 409 }) // Conflict Request
}
if (
const err: HashPasswordError | SaveUserError | FindOneByEmailError
err
instanceof
class HashPasswordError
HashPasswordError
) {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 500 }) // Internal server error
}
if (
const err: SaveUserError | FindOneByEmailError
err
instanceof
class SaveUserError
SaveUserError
) {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 500 }) // Internal server error
}
if (
const err: FindOneByEmailError
err
instanceof
class FindOneByEmailError
FindOneByEmailError
) {
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 500 }) // Internal server error
}
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
msg: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
msg
:
const newUserResult: Err<PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError>
newUserResult
.
error: PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
error
}), {
ResponseInit.status?: number
status
: 500 }) // Internal server error
}
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
const newUserResult: Ok<User>
newUserResult
.
value: User
value
), {
ResponseInit.status?: number
status
: 200 })
}
catch (
function (local var) error: unknown
error
) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.error(message?: any, ...optionalParams: any[]): void

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

@sincev0.1.100

error
(
function (local var) error: unknown
error
)
return new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
function (local var) error: unknown
error
), {
ResponseInit.status?: number
status
: 200 })
}
}

จากโค้ดด้านบน ถ้าเรา hover ที่ function register() ก็จะเห็นว่า มัน return OkOrErr
ถ้า Ok ก็จะได้ User
ถ้า Err ก็จะได้ Error อีก 4 ตัว PasswordInvalid | HashPasswordError | UserAlreadyExist | SaveUserError | FindOneByEmailError
เราก็แค่เอา Error มาเช็คว่ามันเป็น instance ของ Error อะไร แล้วก็ใส่ status code ตามที่เราต้องการโดยอิงจาก Error ที่เกิดขึ้น

จาก code ด้านบนก็ดูดีขึ้นระดับนึงละ ถ้าเกิด Error ขึ้นมาเราจะต้อง log error ออกมาดูเพื่อดู Stack traces error


Effect

จากตัวอย่างด้านบน ถ้าเราใช้ Effect จะทำสิ่งที่ทำด้านบนได้ง่ายขึ้นมากกกกกกก เลยครับ

แต่ก่อนที่จะเอา Effect ไปใส่ ผมขอพามารู้จักกับ Basic Effect กันก่อนนะ

Basic

Effect ประกอบด้วยองค์ประกอบ 3 ส่วน

  1. Success ใช้เมื่อ function หรืองานของเราสำเร็จ
  2. Fail ใช้เมื่อ function หรืองานของเราไม่สำเร็จ เกิด Error
  3. Dependencies เป็นส่วนที่บอกว่าก่อนจะนำ Effect ไปใช้ต้องมี function อะไรก่อนบ้าง
┌─── Represents the success type
│ ┌─── Represents the error type
│ │ ┌─── Represents required dependencies
▼ ▼ ▼
Effect<Success, Fail, Dependencies>

ภาพชุดบน ผมเอามาจาก website ของ Effect นี่ละ

พอเรามี Effect แล้วอยากได้ value จาก Effect เอาจะต้องเอามันไป run ก่อน ผ่าน runtime ของ Effect ซะก่อน เดี๋ยวผมจะอธิบายด้านล่างอีกที

มาดูการสร้าง Effect กันก่อน

เราจะสร้าง Success Effect แบบนี้

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
const ok =
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <string>(value: string) => Effect.Effect<string, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
("Ok")
const ok: Effect.Effect<string, never, never>

จะเห็นว่าตัวแปร ok มี type เป็น Effect.Effect<string, never, never> หมายความว่า

┌─── ถ้ารันผ่านจะได้ `string`
│ ┌─── `never` -> ไม่มีทาง Error
│ │ ┌─── `never` -> ไม่ต้องการ Dependencies ใดๆ เอา Effect ไป run ได้เลย
▼ ▼ ▼
Effect<string, never, never>

ถ้าเอา Effect ตัวนี้ไป run เราจะได้

  1. Success string: จะได้ string กลับมา
  2. Error never: ไม่มี error ฉนั้น Effect นี้ไม่มีทาง Error ได้เลย เพราะเป็น never
  3. Dependencies never: ไม่ต้องการ Dependencies อะไร สามารถนำ Effect นี้ไป run ได้เลย

สร้าง Fail Effect แบบนี้

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
const notOk =
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Ok")
const notOk: Effect.Effect<never, string, never>

จะเห็นว่าตัวแปร notOk มี type เป็นแบบนี้ Effect.Effect<never, string, never> หมายความว่า

┌─── `never` -> ไม่มีทาง run ผ่านเพราะตรงนี้เป็น `never`
│ ┌─── ถ้า Error จะได้ `string`
│ │ ┌─── เอา Effect ไป run ได้เลย เพราะไม่ต้องการ Dependencies ใดๆ
▼ ▼ ▼
Effect<never, string, never>

และส่วนสุดท้าย การสร้าง Dependencies
ผมจะยังไม่พูดถึงใน blog นี้ มันค่อนข้างซับซ้อน และ Beginner อาจยังไม่จำเป็นในตอนนี้ แต่มีสอนใน Free Workshop นะครับ

ฉนั้นเดี๋ยวการเขียน Effect type หลังจากนี้สามารถย่อให้เหลือแค่นี้ก่อนได้ครับ เพราะว่าเราจะไม่ใช้ส่วนของ Dependencies ในตอนนี้ครับ

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
declare const
const okOrNotOk: Effect.Effect<number, string, never>
okOrNotOk
:
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string>

ตัวอย่างเช่น

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
function isNumberInFormOfString(
numberStr: string
numberStr
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string, never> {
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string, never>
const
const num: number
num
=
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.parseInt(string: string, radix?: number): number

Converts A string to an integer.

@paramstring A string to convert into a number.

@paramradix A value between 2 and 36 that specifies the base of the number in string. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.

parseInt
(
numberStr: string
numberStr
)
if (
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.isNaN(number: unknown): boolean

Returns a Boolean value that indicates whether a value is the reserved value NaN (not a number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter to a number. Only values of the type number, that are also NaN, result in true.

@paramnumber A numeric value.

isNaN
(
const num: number
num
) === true) {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Not a number")
}
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <number>(value: number) => Effect.Effect<number, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(
const num: number
num
)
}

How to run Effect

เราจะเอา Effect ไป run ทำได้หลายวิธี แต่ผมมักจะใช้อยู่ 2 วิธีหลักๆ

  1. Effect.runSync()
  2. Effect.runPromise()

การเลือกว่าจะใช้วิธี run แบบไหนมันจะมีวิธีอยู่

ขอให้เข้าใจก่อนว่าปกติเราจะเอา Effect ที่ได้จาก function หลายๆ function มาเรียงต่อกัน

เราจะใช้ Effect.runSync() เมื่อ ทุก function ที่เอามาเรียงต่อๆกัน ไม่มี function ใดเลยที่มี Promise เป็นส่วนประกอบ

เราจะใช้ Effect.runPromise() เมื่อมี function อย่างน้อย 1 function มี Promise เป็นส่วนประกอบ

จากตัวอย่างนี้ 👇

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
function
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string, never>
isNumberInFormOfString
(
numberStr: string
numberStr
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string, never> {
const
const num: number
num
=
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.parseInt(string: string, radix?: number): number

Converts A string to an integer.

@paramstring A string to convert into a number.

@paramradix A value between 2 and 36 that specifies the base of the number in string. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.

parseInt
(
numberStr: string
numberStr
)
if (
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.isNaN(number: unknown): boolean

Returns a Boolean value that indicates whether a value is the reserved value NaN (not a number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter to a number. Only values of the type number, that are also NaN, result in true.

@paramnumber A numeric value.

isNaN
(
const num: number
num
) === true) {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Not a number")
}
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <number>(value: number) => Effect.Effect<number, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(
const num: number
num
)
}

เราก็จะ run ด้วย Effect.runSync() แบบนี้

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
} from "effect"
function
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string, never>
isNumberInFormOfString
(
numberStr: string
numberStr
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string, never> {
const
const num: number
num
=
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.parseInt(string: string, radix?: number): number

Converts A string to an integer.

@paramstring A string to convert into a number.

@paramradix A value between 2 and 36 that specifies the base of the number in string. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.

parseInt
(
numberStr: string
numberStr
)
if (
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.isNaN(number: unknown): boolean

Returns a Boolean value that indicates whether a value is the reserved value NaN (not a number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter to a number. Only values of the type number, that are also NaN, result in true.

@paramnumber A numeric value.

isNaN
(
const num: number
num
) === true) {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Not a number")
}
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <number>(value: number) => Effect.Effect<number, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(
const num: number
num
)
}
// succeed scenario
const program =
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string, never>
isNumberInFormOfString
("42")
const program: Effect.Effect<number, string, never>
const number =
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runSync: <number, string>(effect: Effect.Effect<number, string, never>) => number

Executes an effect synchronously and returns its result.

Use runSync when you are certain that the effect is purely synchronous and will not perform any asynchronous operations. If the effect fails or contains asynchronous tasks, it will throw an error.

@example

import { Effect } from "effect"

// Define a synchronous effect const program = Effect.sync(() => { console.log("Hello, World!") return 1 })

// Execute the effect synchronously const result = Effect.runSync(program) // Output: Hello, World!

console.log(result) // Output: 1

@since2.0.0

runSync
(
const program: Effect.Effect<number, string, never>
program
) // 42
const number: number
// fail scenario
const
const program2: Effect.Effect<number, string, never>
program2
=
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string, never>
isNumberInFormOfString
("abc")
Error: 👇 Effect will throw Error because of got Effect.fail("Not a number")
const
const number2: number
number2
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runSync: <number, string>(effect: Effect.Effect<number, string, never>) => number

Executes an effect synchronously and returns its result.

Use runSync when you are certain that the effect is purely synchronous and will not perform any asynchronous operations. If the effect fails or contains asynchronous tasks, it will throw an error.

@example

import { Effect } from "effect"

// Define a synchronous effect const program = Effect.sync(() => { console.log("Hello, World!") return 1 })

// Execute the effect synchronously const result = Effect.runSync(program) // Output: Hello, World!

console.log(result) // Output: 1

@since2.0.0

runSync
(
const program2: Effect.Effect<number, string, never>
program2
) // "Not a number"

จาก fail scenario ด้านบน เราจะไม่ได้คำตอบของ number2 เพราะว่าเราเอา Effect ไป run แล้วเราไม่ได้จัดการ Error ก่อน Effect.runSync() จะ throw Error เลย
ถ้าไม่อยากให้ Throw Error เราก็ต้อง handle error ก่อน จะ handle error ยังไงเดี๋ยวแสดงให้ดูในตัวอย่าง ด้านล่างนะครับ

Composing Effect

ถ้าเรามี function หลายๆ function ที่ return Effect เหมือนกัน แล้วเราอยากเอา function เหล่านั้นมาทำงานร่วมกันเป็น function ที่ใหญ่ขึ้น แก้ปัญหาที่ใหญ่ขึ้น ให้ใช้
pipe() กับ Effect.andThen()

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g) -> pipe(as, map(f), filter(g))

@example

import { pipe } from "effect/Function" // Alternatively, you can use the following import syntax, as pipe is also conveniently exported from the effect entry point: // import { pipe } from "effect"

const length = (s: string): number => s.length const double = (n: number): number => n * 2 const decrement = (n: number): number => n - 1

assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9)

@since2.0.0

pipe
} from "effect"
function
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string>
isNumberInFormOfString
(
numberStr: string
numberStr
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string> {
const
const num: number
num
=
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.parseInt(string: string, radix?: number): number

Converts A string to an integer.

@paramstring A string to convert into a number.

@paramradix A value between 2 and 36 that specifies the base of the number in string. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.

parseInt
(
numberStr: string
numberStr
)
if (
var Number: NumberConstructor

An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.

Number
.
NumberConstructor.isNaN(number: unknown): boolean

Returns a Boolean value that indicates whether a value is the reserved value NaN (not a number). Unlike the global isNaN(), Number.isNaN() doesn't forcefully convert the parameter to a number. Only values of the type number, that are also NaN, result in true.

@paramnumber A numeric value.

isNaN
(
const num: number
num
) === true) {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Not a number")
}
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <number>(value: number) => Effect.Effect<number, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(
const num: number
num
)
}
function
function divide(a: number, b: number): Effect.Effect<number, string>
divide
(
a: number
a
: number,
b: number
b
: number):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<number, string> {
if (
b: number
b
=== 0) {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <string>(error: string) => Effect.Effect<never, string, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
("Could not use zero as divider")
}
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <number>(value: number) => Effect.Effect<number, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(
a: number
a
/
b: number
b
)
}
function
function sentResultToInternet(a: number): Effect.Effect<void, string>
sentResultToInternet
(
a: number
a
: number):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<void, string> {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <void, string>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<void>;
readonly catch: (error: unknown) => string;
}) => Effect.Effect<void, string, never> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

If the Promise returned by evaluate rejects, the error is caught and the effect fails with an UnknownException.

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

Overload with custom error handling:

Creates an Effect that represents an asynchronous computation that might fail, with custom error mapping.

If the Promise rejects, the catch function maps the error to an error of type E.

@example

import { Effect } from "effect"

// Fetching data from an API that may fail const getTodo = (id: number) => Effect.tryPromise(() => fetch(https://jsonplaceholder.typicode.com/todos/${id}) )

@since2.0.0

tryPromise
({
catch: (error: unknown) => string
catch
: () => "Sent result to internet failed",
try: (signal: AbortSignal) => PromiseLike<void>
try
: async () => {
await
function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
fetch
("/api/result", {
RequestInit.body?: BodyInit
body
:
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
result: number
result
:
a: number
a
}),
RequestInit.method?: string
method
: "POST" })
},
})
}
function
function main(): Promise<void>
main
() {
const
const input: "42"
input
= "42"
const
const program: Effect.Effect<void, never, never>
program
=
pipe<Effect.Effect<number, string, never>, Effect.Effect<number, string, never>, Effect.Effect<void, string, never>, Effect.Effect<void, never, never>>(a: Effect.Effect<...>, ab: (a: Effect.Effect<...>) => Effect.Effect<...>, bc: (b: Effect.Effect<...>) => Effect.Effect<...>, cd: (c: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g) -> pipe(as, map(f), filter(g))

@example

import { pipe } from "effect/Function" // Alternatively, you can use the following import syntax, as pipe is also conveniently exported from the effect entry point: // import { pipe } from "effect"

const length = (s: string): number => s.length const double = (n: number): number => n * 2 const decrement = (n: number): number => n - 1

assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9)

@since2.0.0

pipe
(
function isNumberInFormOfString(numberStr: string): Effect.Effect<number, string>
isNumberInFormOfString
(
const input: "42"
input
),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const andThen: <number, Effect.Effect<number, string, never>>(f: (a: number) => Effect.Effect<number, string, never>) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<...> (+3 overloads)

Executes a sequence of two actions, typically two Effects, where the second action can depend on the result of the first action.

The that action can take various forms:

  • a value
  • a function returning a value
  • a promise
  • a function returning a promise
  • an effect
  • a function returning an effect

@example

import { Effect } from "effect"

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(1))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => s.length))), 2)

assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen(Promise.resolve(1)))), 1) assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen((s) => Promise.resolve(s.length)))), 2)

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(Effect.succeed(1)))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => Effect.succeed(s.length)))), 2)

@since2.0.0

andThen
(
num: number
num
=>
function divide(a: number, b: number): Effect.Effect<number, string>
divide
(
num: number
num
, 20)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const andThen: <number, Effect.Effect<void, string, never>>(f: (a: number) => Effect.Effect<void, string, never>) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<...> (+3 overloads)

Executes a sequence of two actions, typically two Effects, where the second action can depend on the result of the first action.

The that action can take various forms:

  • a value
  • a function returning a value
  • a promise
  • a function returning a promise
  • an effect
  • a function returning an effect

@example

import { Effect } from "effect"

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(1))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => s.length))), 2)

assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen(Promise.resolve(1)))), 1) assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen((s) => Promise.resolve(s.length)))), 2)

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(Effect.succeed(1)))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => Effect.succeed(s.length)))), 2)

@since2.0.0

andThen
(
num: number
num
=>
function sentResultToInternet(a: number): Effect.Effect<void, string>
sentResultToInternet
(
num: number
num
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchAll: <string, void, never, never>(f: (e: string) => Effect.Effect<void, never, never>) => <A, R>(self: Effect.Effect<A, string, R>) => Effect.Effect<void | A, never, R> (+1 overload)

Recovers from all recoverable errors.

Note: that Effect.catchAll will not recover from unrecoverable defects. To recover from both recoverable and unrecoverable errors use Effect.catchAllCause.

@since2.0.0

catchAll
(() =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const void: Effect.Effect<void, never, never>
export void

@since2.0.0

void
),
)
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <void, never>(effect: Effect.Effect<void, never, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<void>

Executes an effect and returns a Promise that resolves with the result.

Use runPromise when working with asynchronous effects and you need to integrate with code that uses Promises. If the effect fails, the returned Promise will be rejected with the error.

@example

import { Effect } from "effect"

// Execute an effect and handle the result with a Promise Effect.runPromise(Effect.succeed(1)).then(console.log) // Output: 1

// Execute a failing effect and handle the rejection Effect.runPromise(Effect.fail("my error")).catch((error) => { console.error("Effect failed with error:", error) })

@since2.0.0

runPromise
(
const program: Effect.Effect<void, never, never>
program
)
}

My recommendation about program flow while using Effect

ผมแนะนำว่าให้ใช้ Effect กับส่วนที่อาจจะเกิด Error ขึ้นได้ เช่น HTTP Request, Database query, parsing อะไรบางอย่างเช่น json, schema
และให้เอา Effect มา compose ต่อๆกันผ่าน Effect.andThen()

สุดท้ายให้ใช้ Effect.run... ในจุดสุดท้ายจริงๆ ผมเรียกจุดนี้ว่า Edge เช่นใน controller ก่อนที่จะ response กลับไปให้ User หรือที่ main function

Let’s change our code to use Effect

เราจะแก้ function ทุกตัวที่อาจจะเกิด Error ให้ใช้ Effect กันแทน
ส่วน function ไหนที่ไม่ได้มี Error ผมแนะนำว่าไม่ต้องเอา Effect ไปครอบนะครับ ตรงนี้มันมีสาเหตุอยู่ เอาไว้เดี๋ยวผมจะเขียน blog เรื่องนี้ออกมาอีกทีนึง

Custom Type กับ Custom Error เราจะไม่ใช้แล้วนะ

type
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
= {
id: string
id
: string
email: string
email
: string
hashedPassword: string
hashedPassword
: string
}
type
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: string
password: string
password
: string
}
type
type Ok<A> = {
type: "Ok";
value: A;
}
Ok
<
function (type parameter) A in type Ok<A>
A
> = {
type: "Ok"
type
: "Ok"
value: A
value
:
function (type parameter) A in type Ok<A>
A
}
type
type Err<E> = {
type: "Err";
error: E;
}
Err
<
function (type parameter) E in type Err<E>
E
> = {
type: "Err"
type
: "Err"
error: E
error
:
function (type parameter) E in type Err<E>
E
}
type
type OkOrErr<A, E> = Ok<A> | Err<E>
OkOrErr
<
function (type parameter) A in type OkOrErr<A, E>
A
,
function (type parameter) E in type OkOrErr<A, E>
E
> =
type Ok<A> = {
type: "Ok";
value: A;
}
Ok
<
function (type parameter) A in type OkOrErr<A, E>
A
> |
type Err<E> = {
type: "Err";
error: E;
}
Err
<
function (type parameter) E in type OkOrErr<A, E>
E
>
class
class CustomError
CustomError
{
constructor(public readonly
CustomError.msg: string
msg
: string, public readonly
CustomError.error?: unknown
error
?: unknown) { }
}

เรามาทำ function ที่มาจาก lib เราจะยังทำการ wrap มันเหมือนเดิม แต่ว่าเอา Effect มา wrap แบบนี้

import {
import Data
Data
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
function flow<A extends ReadonlyArray<unknown>, B = never>(ab: (...a: A) => B): (...a: A) => B (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
,
import Option

@since2.0.0

@since2.0.0

Option
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g) -> pipe(as, map(f), filter(g))

@example

import { pipe } from "effect/Function" // Alternatively, you can use the following import syntax, as pipe is also conveniently exported from the effect entry point: // import { pipe } from "effect"

const length = (s: string): number => s.length const double = (n: number): number => n * 2 const decrement = (n: number): number => n - 1

assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9)

@since2.0.0

pipe
} from "effect"
// assume hashPassword function from lib like bcrypt or argon2
declare function
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<string>
class
class HashPasswordError
HashPasswordError
extends
import Data
Data
.
const TaggedError: <"HashPasswordError">(tag: "HashPasswordError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
("HashPasswordError")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
function
function hashPasswordWrap(password: string): Effect.Effect<string, HashPasswordError>
hashPasswordWrap
(
password: string
password
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<string,
class HashPasswordError
HashPasswordError
> {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <string, HashPasswordError>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<string>;
readonly catch: (error: unknown) => HashPasswordError;
}) => Effect.Effect<...> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

If the Promise returned by evaluate rejects, the error is caught and the effect fails with an UnknownException.

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

Overload with custom error handling:

Creates an Effect that represents an asynchronous computation that might fail, with custom error mapping.

If the Promise rejects, the catch function maps the error to an error of type E.

@example

import { Effect } from "effect"

// Fetching data from an API that may fail const getTodo = (id: number) => Effect.tryPromise(() => fetch(https://jsonplaceholder.typicode.com/todos/${id}) )

@since2.0.0

tryPromise
({
catch: (error: unknown) => HashPasswordError
catch
:
error: unknown
error
=> new
constructor HashPasswordError<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): HashPasswordError
HashPasswordError
({
error?: unknown
error
,
msg: string
msg
: "hash password failed" }),
try: (signal: AbortSignal) => PromiseLike<string>
try
: () =>
function hashPassword(password: string): Promise<string>
hashPassword
(
password: string
password
),
})
}

เราจะสร้าง wrapper function มาครอบพวก function ที่มาจาก lib อื่นๆ ส่วนที่เราคุมไม่ได้ จำกัดมันไว้แค่ในนั้น ในที่นี้ เราสร้าง function ที่ชื่อว่า hashPasswordWrap() ซึ่ง function ที่เราเอามาจาก lib เนี่ยมันเป็น Promise-based เราก็เลยใช้ Effect.tryPromise() ครอบอีกที

แล้วก็ยังสร้าง Error ตัวเดิม แต่เปลี่ยนวิธีการสร้าง โดยเราจะใช้
Data.TaggedError("Error tag")<Custom Error Record>
ข้อดีของการสร้าง Error แบบนี้คือมันติด Tag ได้ การติด tag จะมีประโยชน์ ตอนที่จัดการกับ Error
การติด Tag ให้ Error ควรจะเป็น unique string นะ คือมันไม่ควรจะไปซ้ำกับ Error Tag อื่นๆ โดยมากผมมักจะใช้ชื่อเดียวกับชื่อ Class จะได้ไม่งงตอนเอาไปใช้ด้วย

จะเห็นว่า function hashPasswordWrap() มัน return Effect ที่
ถ้าทำงานสำเร็จเราจะได้ string
แต่ถ้าเกิด Error เราจะได้ HashPasswordError class

มาดูกันต่อ

import {
import Data
Data
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
function flow<A extends ReadonlyArray<unknown>, B = never>(ab: (...a: A) => B): (...a: A) => B (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
,
import Option

@since2.0.0

@since2.0.0

Option
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g) -> pipe(as, map(f), filter(g))

@example

import { pipe } from "effect/Function" // Alternatively, you can use the following import syntax, as pipe is also conveniently exported from the effect entry point: // import { pipe } from "effect"

const length = (s: string): number => s.length const double = (n: number): number => n * 2 const decrement = (n: number): number => n - 1

assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9)

@since2.0.0

pipe
} from "effect"
// assume isEmailExist function from our another module that query the User table
declare function
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null>
class
class FindOneByEmailError
FindOneByEmailError
extends
import Data
Data
.
const TaggedError: <"FindOneByEmailError">(tag: "FindOneByEmailError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
("FindOneByEmailError")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
function findOneByEmailWrap(
email: string
email
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<
import Option

@since2.0.0

@since2.0.0

Option
.
type Option<A> = Option.None<A> | Option.Some<A>

@since2.0.0

@since2.0.0

Option
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>,
class FindOneByEmailError
FindOneByEmailError
> {
function findOneByEmailWrap(email: string): Effect.Effect<Option.Option<User>, FindOneByEmailError>
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <User | null, FindOneByEmailError>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<User | null>;
readonly catch: (error: unknown) => FindOneByEmailError;
}) => Effect.Effect<...> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

If the Promise returned by evaluate rejects, the error is caught and the effect fails with an UnknownException.

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

Overload with custom error handling:

Creates an Effect that represents an asynchronous computation that might fail, with custom error mapping.

If the Promise rejects, the catch function maps the error to an error of type E.

@example

import { Effect } from "effect"

// Fetching data from an API that may fail const getTodo = (id: number) => Effect.tryPromise(() => fetch(https://jsonplaceholder.typicode.com/todos/${id}) )

@since2.0.0

tryPromise
({
catch: (error: unknown) => FindOneByEmailError
catch
:
error: unknown
error
=> new
constructor FindOneByEmailError<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): FindOneByEmailError
FindOneByEmailError
({
error?: unknown
error
,
msg: string
msg
: "find one by email failed" }),
try: (signal: AbortSignal) => PromiseLike<User | null>
try
: () =>
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
),
}).
Pipeable.pipe<Effect.Effect<User | null, FindOneByEmailError, never>, Effect.Effect<Option.Option<User>, FindOneByEmailError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const map: <User | null, Option.Option<User>>(f: (a: User | null) => Option.Option<User>) => <E, R>(self: Effect.Effect<User | null, E, R>) => Effect.Effect<...> (+1 overload)

@since2.0.0

map
(
import Option

@since2.0.0

@since2.0.0

Option
.
const fromNullable: <A>(nullableValue: A) => Option.Option<NonNullable<A>>

Constructs a new Option from a nullable type. If the value is null or undefined, returns None, otherwise returns the value wrapped in a Some.

@paramnullableValue - The nullable value to be converted to an Option.

@example

import { Option } from "effect"

assert.deepStrictEqual(Option.fromNullable(undefined), Option.none()) assert.deepStrictEqual(Option.fromNullable(null), Option.none()) assert.deepStrictEqual(Option.fromNullable(1), Option.some(1))

@since2.0.0

fromNullable
),
)
}
// assume saveUser function is in our another module that save use into the database
declare function
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>
class
class SaveUserError
SaveUserError
extends
import Data
Data
.
const TaggedError: <"SaveUserError">(tag: "SaveUserError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & {
...;
} & Readonly<...>

@since2.0.0

TaggedError
("SaveUserError")<{
error?: unknown
error
?: unknown,
msg: string
msg
: string }> { }
function saveUserWrap(
user: Omit<User, "id">
user
:
type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

Construct a type with the properties of T except for those in type K.

Omit
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
, "id">):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
,
class SaveUserError
SaveUserError
> {
function saveUserWrap(user: Omit<User, "id">): Effect.Effect<User, SaveUserError>
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <User, SaveUserError>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<User>;
readonly catch: (error: unknown) => SaveUserError;
}) => Effect.Effect<...> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

If the Promise returned by evaluate rejects, the error is caught and the effect fails with an UnknownException.

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

Overload with custom error handling:

Creates an Effect that represents an asynchronous computation that might fail, with custom error mapping.

If the Promise rejects, the catch function maps the error to an error of type E.

@example

import { Effect } from "effect"

// Fetching data from an API that may fail const getTodo = (id: number) => Effect.tryPromise(() => fetch(https://jsonplaceholder.typicode.com/todos/${id}) )

@since2.0.0

tryPromise
({
catch: (error: unknown) => SaveUserError
catch
:
error: unknown
error
=> new
constructor SaveUserError<{
error?: unknown;
msg: string;
}>(args: {
readonly error?: unknown;
readonly msg: string;
}): SaveUserError
SaveUserError
({
error?: unknown
error
,
msg: string
msg
: "save user failed" }),
try: (signal: AbortSignal) => PromiseLike<User>
try
: () =>
function saveUser(user: Omit<User, "id">): Promise<User>
saveUser
(
user: Omit<User, "id">
user
),
})
}

ใน function findOneByEmailWrap() ผมก็ทำเหมือนเดิมเลย

แต่ก็มีส่วนที่เขียนเพิ่มด้วย คือหลังจากจบ tryPromise() เราจะได้ผลลัพธ์ เป็น User | null แต่เราไม่ควรใช้ null ผมก็ใส่ .pipe() เพื่อเอาผลลัพธ์ที่ได้ มาเข้า function แปลงอีกทีนึง
เป้าหมายของผมก็ง่ายๆ คือถ้าเป็น null ก็ให้แปลงเป็น Option แทน ถ้ามี value ก็จะได้ Option.Some ถ้าเป็น null ก็จะได้ Option.None
ตรงนี้ผมจะใช้ Effect.map() แล้วใส่ Option.fromNullable ลงไปมันก็จะเอา User | null แปลงเป็น Option ละ

มาทำ function ที่ใช้ validate ทั้งหลายกันต่อง

import {
import Data
Data
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
function flow<A extends ReadonlyArray<unknown>, B = never>(ab: (...a: A) => B): (...a: A) => B (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
,
import Match
Match
,
import Option

@since2.0.0

@since2.0.0

Option
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g) -> pipe(as, map(f), filter(g))

@example

import { pipe } from "effect/Function" // Alternatively, you can use the following import syntax, as pipe is also conveniently exported from the effect entry point: // import { pipe } from "effect"

const length = (s: string): number => s.length const double = (n: number): number => n * 2 const decrement = (n: number): number => n - 1

assert.deepStrictEqual(pipe(length("hello"), double, decrement), 9)

@since2.0.0

pipe
} from "effect"
function
function validateEmail(email: string): boolean
validateEmail
(
email: string
email
: string): boolean {
const
const emailRegex: RegExp
emailRegex
= /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
return
const emailRegex: RegExp
emailRegex
.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
email: string
email
)
}
class
class EmailInvalid
EmailInvalid
extends
import Data
Data
.
const TaggedError: <"EmailInvalid">(tag: "EmailInvalid") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & {
...;
} & Readonly<...>

@since2.0.0

TaggedError
("EmailInvalid")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
const validateEmailEffect =
flow<[email: string], boolean, Effect.Effect<boolean, EmailInvalid, never>>(ab: (email: string) => boolean, bc: (b: boolean) => Effect.Effect<boolean, EmailInvalid, never>): (email: string) => Effect.Effect<...> (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
(
const validateEmailEffect: (email: string) => Effect.Effect<boolean, EmailInvalid, never>
function validateEmail(email: string): boolean
validateEmail
,
import Match
Match
.
const type: <boolean>() => Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>

@since1.0.0

type
<boolean>().
Pipeable.pipe<Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>, Match.Matcher<boolean, Match.Types.Without<false>, true, Effect.Effect<never, EmailInvalid, never>, never, any>, Match.Matcher<...>, (u: boolean) => Effect.Effect<...>>(this: Match.Matcher<...>, ab: (_: Match.Matcher<...>) => Match.Matcher<...>, bc: (_: Match.Matcher<...>) => Match.Matcher<...>, cd: (_: Match.Matcher<...>) => (u: boolean) => Effect.Effect<...>): (u: boolean) => Effect.Effect<...> (+21 overloads)
pipe
(
import Match
Match
.
const when: <boolean, false, any, () => Effect.Effect<never, EmailInvalid, never>>(pattern: false, f: () => Effect.Effect<never, EmailInvalid, never>) => <I, F, A, Pr>(self: Match.Matcher<...>) => Match.Matcher<...>

@since1.0.0

when
(false, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <EmailInvalid>(error: EmailInvalid) => Effect.Effect<never, EmailInvalid, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
(new
constructor EmailInvalid<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): EmailInvalid
EmailInvalid
({
msg: string
msg
: "email is invalid" }))),
import Match
Match
.
const when: <true, true, any, () => Effect.Effect<boolean, never, never>>(pattern: true, f: () => Effect.Effect<boolean, never, never>) => <I, F, A, Pr>(self: Match.Matcher<I, F, true, A, Pr, any>) => Match.Matcher<...>

@since1.0.0

when
(true, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <boolean>(value: boolean) => Effect.Effect<boolean, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(true)),
import Match
Match
.
const exhaustive: <I, F, A, Pr, Ret>(self: Match.Matcher<I, F, never, A, Pr, Ret>) => [Pr] extends [never] ? (u: I) => Unify<A> : Unify<A>

@since1.0.0

exhaustive
,
),
)
class
class PasswordInvalid
PasswordInvalid
extends
import Data
Data
.
const TaggedError: <"PasswordInvalid">(tag: "PasswordInvalid") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
("PasswordInvalid")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
function
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
(
password: string
password
: string): boolean {
return
password: string
password
.
String.length: number

Returns the length of a String object.

length
>= 8
}
const isPassword8CharLongEffect =
flow<[password: string], boolean, Effect.Effect<boolean, PasswordInvalid, never>>(ab: (password: string) => boolean, bc: (b: boolean) => Effect.Effect<boolean, PasswordInvalid, never>): (password: string) => Effect.Effect<...> (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
(
const isPassword8CharLongEffect: (password: string) => Effect.Effect<boolean, PasswordInvalid, never>
function isPassword8CharLong(password: string): boolean
isPassword8CharLong
,
import Match
Match
.
const type: <boolean>() => Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>

@since1.0.0

type
<boolean>().
Pipeable.pipe<Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>, Match.Matcher<boolean, Match.Types.Without<false>, true, Effect.Effect<never, PasswordInvalid, never>, never, any>, Match.Matcher<...>, (u: boolean) => Effect.Effect<...>>(this: Match.Matcher<...>, ab: (_: Match.Matcher<...>) => Match.Matcher<...>, bc: (_: Match.Matcher<...>) => Match.Matcher<...>, cd: (_: Match.Matcher<...>) => (u: boolean) => Effect.Effect<...>): (u: boolean) => Effect.Effect<...> (+21 overloads)
pipe
(
import Match
Match
.
const when: <boolean, false, any, () => Effect.Effect<never, PasswordInvalid, never>>(pattern: false, f: () => Effect.Effect<never, PasswordInvalid, never>) => <I, F, A, Pr>(self: Match.Matcher<...>) => Match.Matcher<...>

@since1.0.0

when
(false, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <PasswordInvalid>(error: PasswordInvalid) => Effect.Effect<never, PasswordInvalid, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
(new
constructor PasswordInvalid<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): PasswordInvalid
PasswordInvalid
({
msg: string
msg
: "password need to be 8 characters long" }))),
import Match
Match
.
const when: <true, true, any, () => Effect.Effect<boolean, never, never>>(pattern: true, f: () => Effect.Effect<boolean, never, never>) => <I, F, A, Pr>(self: Match.Matcher<I, F, true, A, Pr, any>) => Match.Matcher<...>

@since1.0.0

when
(true, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <boolean>(value: boolean) => Effect.Effect<boolean, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(true)),
import Match
Match
.
const exhaustive: <I, F, A, Pr, Ret>(self: Match.Matcher<I, F, never, A, Pr, Ret>) => [Pr] extends [never] ? (u: I) => Unify<A> : Unify<A>

@since1.0.0

exhaustive
,
),
)
function
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
(
password: string
password
: string): boolean {
return /[^a-z0-9]/i.
RegExp.test(string: string): boolean

Returns a Boolean value that indicates whether or not a pattern exists in a searched string.

@paramstring String on which to perform the search.

test
(
password: string
password
)
}
const isPasswordContainsSpecialCharEffect: (
password: string
password
: string) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<boolean,
class PasswordInvalid
PasswordInvalid
> =
flow<[password: string], boolean, Effect.Effect<boolean, PasswordInvalid, never>>(ab: (password: string) => boolean, bc: (b: boolean) => Effect.Effect<boolean, PasswordInvalid, never>): (password: string) => Effect.Effect<...> (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
(
function isPasswordContainsSpecialChar(password: string): boolean
isPasswordContainsSpecialChar
,
import Match
Match
.
const type: <boolean>() => Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>

@since1.0.0

type
<boolean>().
Pipeable.pipe<Match.Matcher<boolean, Match.Types.Without<never>, boolean, never, never, any>, Match.Matcher<boolean, Match.Types.Without<false>, true, Effect.Effect<never, PasswordInvalid, never>, never, any>, Match.Matcher<...>, (u: boolean) => Effect.Effect<...>>(this: Match.Matcher<...>, ab: (_: Match.Matcher<...>) => Match.Matcher<...>, bc: (_: Match.Matcher<...>) => Match.Matcher<...>, cd: (_: Match.Matcher<...>) => (u: boolean) => Effect.Effect<...>): (u: boolean) => Effect.Effect<...> (+21 overloads)
pipe
(
const isPasswordContainsSpecialCharEffect: (password: string) => Effect.Effect<boolean, PasswordInvalid>
import Match
Match
.
const when: <boolean, false, any, () => Effect.Effect<never, PasswordInvalid, never>>(pattern: false, f: () => Effect.Effect<never, PasswordInvalid, never>) => <I, F, A, Pr>(self: Match.Matcher<...>) => Match.Matcher<...>

@since1.0.0

when
(false, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <PasswordInvalid>(error: PasswordInvalid) => Effect.Effect<never, PasswordInvalid, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
(new
constructor PasswordInvalid<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): PasswordInvalid
PasswordInvalid
({
msg: string
msg
: "password need to be contains at least one special character" }))),
import Match
Match
.
const when: <true, true, any, () => Effect.Effect<boolean, never, never>>(pattern: true, f: () => Effect.Effect<boolean, never, never>) => <I, F, A, Pr>(self: Match.Matcher<I, F, true, A, Pr, any>) => Match.Matcher<...>

@since1.0.0

when
(true, () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <boolean>(value: boolean) => Effect.Effect<boolean, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(true)),
import Match
Match
.
const exhaustive: <I, F, A, Pr, Ret>(self: Match.Matcher<I, F, never, A, Pr, Ret>) => [Pr] extends [never] ? (u: I) => Unify<A> : Unify<A>

@since1.0.0

exhaustive
,
))

จากโค้ดด้านบน ผมสร้าง function ครอบ function ที่ใช้ verfiy email กับ verify password ไว้แล้วให้ return Effect
ผมใช้ flow() หลายคนอาจจะไม่คุ้นเคย ผมเล่าสั้นๆเกี่ยวกับ flow() ว่า มาจาก FP มันเป็นการเอา function เดิมที่มีอยู่แล้ว มาเขียนเสริมเข้าไปกับอีก function หนึ่งกลายเป็น function ใหม่ที่รับ argument แบบเดิมเด๊ะๆ
พยายามอธิบายไม่รู้ว่าจะงงหนักกว่าเก่าหรือเปล่า เอาไว้ผมจะเขียน blog เล่าเกี่ยวกับ flow() นี้ให้แบบละเอียดอีกทีนะครับ note ว่ามันจะคล้ายๆกับ pipe()

ยังไม่หมด จะเห็นว่าผมใช้ Match ด้วย หลายคนก็อาจจะไม่คุ้นอีกแล้วหรอ
เอาสั้นๆมันก็เหมือนกับ switch-case รุ่น upgrade ที่ powerful มากๆ
ถ้ามีเวลาก็จะเขียน blog เกี่ยวกับตัวนี้ให้เช่นกันครับ

มาทำกันต่อ อีกนิดเดียวครับ

ผมจะทำ function isEmailExist() โดยจะเอาไปใช้ดูว่า email นี้มีอยู่ใน Database แล้วหรือยัง

// assume isEmailExist function from our another module that query the User table
declare function
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
| null>
class
class FindOneByEmailError
FindOneByEmailError
extends
import Data
Data
.
const TaggedError: <"FindOneByEmailError">(tag: "FindOneByEmailError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
("FindOneByEmailError")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
function
function findOneByEmailWrap(email: string): Effect.Effect<Option.Option<User>, FindOneByEmailError>
findOneByEmailWrap
(
email: string
email
: string):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that lazily describes a workflow or job. The workflow requires some context R, and may fail with an error of type E, or succeed with a value of type A.

Effect values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability.

To run an Effect value, you need a Runtime, which is a type that is capable of executing Effect values.

@since2.0.0

@since2.0.0

Effect
<
import Option

@since2.0.0

@since2.0.0

Option
.
type Option<A> = Option.None<A> | Option.Some<A>

@since2.0.0

@since2.0.0

Option
<
type User = {
id: string;
email: string;
hashedPassword: string;
}
User
>,
class FindOneByEmailError
FindOneByEmailError
> {
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <User | null, FindOneByEmailError>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<User | null>;
readonly catch: (error: unknown) => FindOneByEmailError;
}) => Effect.Effect<...> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

If the Promise returned by evaluate rejects, the error is caught and the effect fails with an UnknownException.

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

Overload with custom error handling:

Creates an Effect that represents an asynchronous computation that might fail, with custom error mapping.

If the Promise rejects, the catch function maps the error to an error of type E.

@example

import { Effect } from "effect"

// Fetching data from an API that may fail const getTodo = (id: number) => Effect.tryPromise(() => fetch(https://jsonplaceholder.typicode.com/todos/${id}) )

@since2.0.0

tryPromise
({
catch: (error: unknown) => FindOneByEmailError
catch
:
error: unknown
error
=> new
constructor FindOneByEmailError<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): FindOneByEmailError
FindOneByEmailError
({
error?: unknown
error
,
msg: string
msg
: "find one by email failed" }),
try: (signal: AbortSignal) => PromiseLike<User | null>
try
: () =>
function findOneByEmail(email: string): Promise<User | null>
findOneByEmail
(
email: string
email
),
}).
Pipeable.pipe<Effect.Effect<User | null, FindOneByEmailError, never>, Effect.Effect<Option.Option<User>, FindOneByEmailError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const map: <User | null, Option.Option<User>>(f: (a: User | null) => Option.Option<User>) => <E, R>(self: Effect.Effect<User | null, E, R>) => Effect.Effect<...> (+1 overload)

@since2.0.0

map
(
import Option

@since2.0.0

@since2.0.0

Option
.
const fromNullable: <A>(nullableValue: A) => Option.Option<NonNullable<A>>

Constructs a new Option from a nullable type. If the value is null or undefined, returns None, otherwise returns the value wrapped in a Some.

@paramnullableValue - The nullable value to be converted to an Option.

@example

import { Option } from "effect"

assert.deepStrictEqual(Option.fromNullable(undefined), Option.none()) assert.deepStrictEqual(Option.fromNullable(null), Option.none()) assert.deepStrictEqual(Option.fromNullable(1), Option.some(1))

@since2.0.0

fromNullable
),
)
}
class
class UserAlreadyExist
UserAlreadyExist
extends
import Data
Data
.
const TaggedError: <"UserAlreadyExist">(tag: "UserAlreadyExist") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & ... 1 more ... & Readonly<...>

@since2.0.0

TaggedError
("UserAlreadyExist")<{
msg: string
msg
: string,
error?: unknown
error
?: unknown }> { }
const
const isEmailExist: (email: string) => Effect.Effect<void, FindOneByEmailError | UserAlreadyExist, never>
isEmailExist
=
flow<[email: string], Effect.Effect<Option.Option<User>, FindOneByEmailError, never>, Effect.Effect<void, FindOneByEmailError | UserAlreadyExist, never>>(ab: (email: string) => Effect.Effect<...>, bc: (b: Effect.Effect<...>) => Effect.Effect<...>): (email: string) => Effect.Effect<...> (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"

const len = (s: string): number => s.length const double = (n: number): number => n * 2

const f = flow(len, double)

assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
(
function findOneByEmailWrap(email: string): Effect.Effect<Option.Option<User>, FindOneByEmailError>
findOneByEmailWrap
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const andThen: <Option.Option<User>, Effect.Effect<void, never, never> | Effect.Effect<never, UserAlreadyExist, never>>(f: (a: Option.Option<User>) => Effect.Effect<...> | Effect.Effect<...>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+3 overloads)

Executes a sequence of two actions, typically two Effects, where the second action can depend on the result of the first action.

The that action can take various forms:

  • a value
  • a function returning a value
  • a promise
  • a function returning a promise
  • an effect
  • a function returning an effect

@example

import { Effect } from "effect"

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(1))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => s.length))), 2)

assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen(Promise.resolve(1)))), 1) assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen((s) => Promise.resolve(s.length)))), 2)

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(Effect.succeed(1)))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => Effect.succeed(s.length)))), 2)

@since2.0.0

andThen
(
import Option

@since2.0.0

@since2.0.0

Option
.
const match: <Effect.Effect<void, never, never>, User, Effect.Effect<never, UserAlreadyExist, never>>(options: {
readonly onNone: LazyArg<Effect.Effect<void, never, never>>;
readonly onSome: (a: User) => Effect.Effect<...>;
}) => (self: Option.Option<...>) => Effect.Effect<...> | Effect.Effect<...> (+1 overload)

Matches the given Option and returns either the provided onNone value or the result of the provided onSome function when passed the Option's value.

@paramself - The Option to match

@paramonNone - The value to be returned if the Option is None

@paramonSome - The function to be called if the Option is Some, it will be passed the Option's value and its result will be returned

@example

import { pipe, Option } from "effect"

assert.deepStrictEqual( pipe(Option.some(1), Option.match({ onNone: () => 'a none', onSome: (a) => a some containing ${a} })), 'a some containing 1' )

assert.deepStrictEqual( pipe(Option.none(), Option.match({ onNone: () => 'a none', onSome: (a) => a some containing ${a} })), 'a none' )

@since2.0.0

match
({
onNone: LazyArg<Effect.Effect<void, never, never>>
onNone
: () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const void: Effect.Effect<void, never, never>
export void

@since2.0.0

void
,
onSome: (a: User) => Effect.Effect<never, UserAlreadyExist, never>
onSome
: () =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <UserAlreadyExist>(error: UserAlreadyExist) => Effect.Effect<never, UserAlreadyExist, never>

Creates an Effect that represents a recoverable error.

This Effect does not succeed but instead fails with the provided error. The failure can be of any type, and will propagate through the effect pipeline unless handled.

Use this function when you want to explicitly signal an error in an Effect computation. The failed effect can later be handled with functions like

catchAll

or

catchTag

.

@example

import { Effect } from "effect"

// Example of creating a failed effect const failedEffect = Effect.fail("Something went wrong")

// Handle the failure failedEffect.pipe( Effect.catchAll((error) => Effect.succeed(Recovered from: ${error})), Effect.runPromise ).then(console.log) // Output: "Recovered from: Something went wrong"

@since2.0.0

fail
(new
constructor UserAlreadyExist<{
msg: string;
error?: unknown;
}>(args: {
readonly msg: string;
readonly error?: unknown;
}): UserAlreadyExist
UserAlreadyExist
({
msg: string
msg
: "email already exist" })),
})),
)

จาก code ด้านบน ผมเอา findOneByEmailWrap() มาใช้
ถ้าเจอว่ามี User ที่มี Email อยู่แล้ว จะให้ Fail ไปเลย
ถ้าไม่เจอเราจะให้ return Effect.void ซึ่ง Effect.void มันจะเป็น Succeed ตัวนึง แต่ไม่ได้มีค่าอะไรด้านในกล่อง

สุดท้ายเราจะเอาทุกๆ functions มารวมกันเป็น register() แบบนี้

function register(
user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
) {
function register(user: UserInput): Effect.Effect<User, EmailInvalid | PasswordInvalid | HashPasswordError | FindOneByEmailError | UserAlreadyExist | SaveUserError, never>
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const Do: Effect.Effect<{}, never, never>

The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like bind and let.

Here's how the do simulation works:

  1. Start the do simulation using the Do value
  2. Within the do simulation scope, you can use the bind function to define variables and bind them to Effect values
  3. You can accumulate multiple bind statements to define multiple variables within the scope
  4. Inside the do simulation scope, you can also use the let function to define variables and bind them to simple values

@seebind

@seebindTo

@seelet_let

@example

import { Effect, pipe } from "effect"

const result = pipe( Effect.Do, Effect.bind("x", () => Effect.succeed(2)), Effect.bind("y", () => Effect.succeed(3)), Effect.let("sum", ({ x, y }) => x + y) ) assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 })

@since2.0.0

Do
.
Pipeable.pipe<Effect.Effect<{}, never, never>, Effect.Effect<{}, EmailInvalid, never>, Effect.Effect<{}, EmailInvalid | PasswordInvalid, never>, Effect.Effect<...>, Effect.Effect<...>, Effect.Effect<...>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>, cd: (_: Effect.Effect<...>) => Effect.Effect<...>, de: (_: Effect.Effect<...>) => Effect.Effect<...>, ef: (_: Effect.Effect<...>) => Effect.Effect<...>, fg: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tap: <{}, Effect.Effect<boolean, EmailInvalid, never>>(f: (a: {}) => Effect.Effect<boolean, EmailInvalid, never>) => <E, R>(self: Effect.Effect<{}, E, R>) => Effect.Effect<...> (+7 overloads)

@since2.0.0

tap
(() =>
const validateEmailEffect: (email: string) => Effect.Effect<boolean, EmailInvalid, never>
validateEmailEffect
(
user: UserInput
user
.
email: string
email
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tap: <{}, Effect.Effect<boolean, PasswordInvalid, never>>(f: (a: {}) => Effect.Effect<boolean, PasswordInvalid, never>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+7 overloads)

@since2.0.0

tap
(() =>
const isPassword8CharLongEffect: (password: string) => Effect.Effect<boolean, PasswordInvalid, never>
isPassword8CharLongEffect
(
user: UserInput
user
.
password: string
password
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tap: <{}, Effect.Effect<boolean, PasswordInvalid, never>>(f: (a: {}) => Effect.Effect<boolean, PasswordInvalid, never>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+7 overloads)

@since2.0.0

tap
(() =>
const isPasswordContainsSpecialCharEffect: (password: string) => Effect.Effect<boolean, PasswordInvalid>
isPasswordContainsSpecialCharEffect
(
user: UserInput
user
.
password: string
password
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const bind: <"hashedPassword", {}, string, HashPasswordError, never>(name: "hashedPassword", f: (a: {}) => Effect.Effect<string, HashPasswordError, never>) => <E1, R1>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

The "do simulation" in Effect allows you to write code in a more declarative style, similar to the "do notation" in other programming languages. It provides a way to define variables and perform operations on them using functions like bind and let.

Here's how the do simulation works:

  1. Start the do simulation using the Do value
  2. Within the do simulation scope, you can use the bind function to define variables and bind them to Effect values
  3. You can accumulate multiple bind statements to define multiple variables within the scope
  4. Inside the do simulation scope, you can also use the let function to define variables and bind them to simple values

@seeDo

@seebindTo

@seelet_let

@example

import { Effect, pipe } from "effect"

const result = pipe( Effect.Do, Effect.bind("x", () => Effect.succeed(2)), Effect.bind("y", () => Effect.succeed(3)), Effect.let("sum", ({ x, y }) => x + y) ) assert.deepStrictEqual(Effect.runSync(result), { x: 2, y: 3, sum: 5 })

@since2.0.0

bind
("hashedPassword", () =>
function hashPasswordWrap(password: string): Effect.Effect<string, HashPasswordError>
hashPasswordWrap
(
user: UserInput
user
.
password: string
password
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tap: <{
hashedPassword: string;
}, Effect.Effect<void, FindOneByEmailError | UserAlreadyExist, never>>(f: (a: {
hashedPassword: string;
}) => Effect.Effect<...>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+7 overloads)

@since2.0.0

tap
(() =>
const isEmailExist: (email: string) => Effect.Effect<void, FindOneByEmailError | UserAlreadyExist, never>
isEmailExist
(
user: UserInput
user
.
email: string
email
)),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const andThen: <{
hashedPassword: string;
}, Effect.Effect<User, SaveUserError, never>>(f: (a: {
hashedPassword: string;
}) => Effect.Effect<User, SaveUserError, never>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+3 overloads)

Executes a sequence of two actions, typically two Effects, where the second action can depend on the result of the first action.

The that action can take various forms:

  • a value
  • a function returning a value
  • a promise
  • a function returning a promise
  • an effect
  • a function returning an effect

@example

import { Effect } from "effect"

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(1))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => s.length))), 2)

assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen(Promise.resolve(1)))), 1) assert.deepStrictEqual(await Effect.runPromise(Effect.succeed("aa").pipe(Effect.andThen((s) => Promise.resolve(s.length)))), 2)

assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen(Effect.succeed(1)))), 1) assert.deepStrictEqual(Effect.runSync(Effect.succeed("aa").pipe(Effect.andThen((s) => Effect.succeed(s.length)))), 2)

@since2.0.0

andThen
(({
hashedPassword: string
hashedPassword
}) =>
function saveUserWrap(user: Omit<User, "id">): Effect.Effect<User, SaveUserError>
saveUserWrap
({
email: string
email
:
user: UserInput
user
.
email: string
email
,
hashedPassword: string
hashedPassword
,
})),
)
}

จะเห็นว่าเราเขียน register() เราเอา functions หลายๆตัวมาเขียนต่อๆกัน function หนึ่งทำงานเสร็จก็ไปทำอีก function นึง
โดยเราไม่ต้องสนใจเลยว่ามันจะเกิด Error หรือเปล่า จะเห็นว่าเราไม่ได้ handle Error อะไรตรงนี้เลย
การใช้ Effect จะทำให้เรา focus กับ logic ที่เราต้องการเพียงอย่างเดียว

จะเห็นว่ามันสั้นลงเยอะมากๆ โค้ดดูสวยมากขึ้น

Effect.tap() คือ function ที่เอาไว้แอบดูของด้านใน Effect ถ้าเรา return Effect.succeed จากใน tap() มันจะถูก ignore ไป ไม่ถูกนับ ไม่ได้มีผลอะไรต่อการทำงานของ function ถัดไป
แต่ถ้า tap() ดัน return Effect.fail มันจะทำให้ register() fail ไปเลย

Effect.bind() ใช้แทนการสร้างตัวแปร (ใครเคยเขียน Haskell มันคือ Do notation, ใน Scala น่าจะเป็น for comprehensions มั้งนะ) ผลลัพธ์ที่ได้จาก bind() จะถูกเก็บอยู่ใน object
function ถัดไปสามารถนำมาใช้ได้ โดยมันจะอยู่ใน object ตาม key ที่เราใส่ไว้ใน bind(<key>, <function>) ถ้าดูจากตัวอย่าง key ก็คือ hashedPassword

Effect.andThen() ก็คือแกะของขวัญออกจาก Effect เอามาทำอะไรสักอย่าง แล้วทำงาน function จากนั้นก็เอาผลลัพธ์ที่ได้ใส่กลับลงกล่อง Effect

การที่เราเอา functions หลายๆตัวมาทำงานต่อๆกัน แล้วมี function ตัวใดตัวนึงเกิด fail
จะทำให้ function ที่เหลือไม่ถูกทำงาน flow จะจบแค่นั้นเลย เราเรียกการทำงานแบบนี้ว่า Short-Circuiting

สุดท้ายเราก็จะเอา register() มาใช้ที่ controller แบบนี้

const
const user: UserInput
user
:
type UserInput = {
email: string;
password: string;
}
UserInput
= {
email: string
email
: "[email protected]",
password: string
password
: "test",
}
async function
function registerController(): Promise<Response>
registerController
() {
const program =
function register(user: UserInput): Effect.Effect<User, EmailInvalid | PasswordInvalid | HashPasswordError | FindOneByEmailError | UserAlreadyExist | SaveUserError, never>
register
(
const user: UserInput
user
).
Pipeable.pipe<Effect.Effect<User, EmailInvalid | PasswordInvalid | HashPasswordError | FindOneByEmailError | UserAlreadyExist | SaveUserError, never>, Effect.Effect<...>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
const program: Effect.Effect<Response, never, never>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const map: <User, Response>(f: (a: User) => Response) => <E, R>(self: Effect.Effect<User, E, R>) => Effect.Effect<Response, E, R> (+1 overload)

@since2.0.0

map
(
user: User
user
=> new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
(
user: User
user
), {
ResponseInit.status?: number
status
: 201 })),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchTags: <EmailInvalid | PasswordInvalid | HashPasswordError | FindOneByEmailError | UserAlreadyExist | SaveUserError, {
...;
}>(cases: {
...;
}) => <A, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Recovers from the specified tagged errors.

@since2.0.0

catchTags
({
type EmailInvalid: (e: EmailInvalid) => Effect.Effect<Response, never, never>
EmailInvalid
:
e: EmailInvalid
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: EmailInvalid
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 400 })),
type FindOneByEmailError: (e: FindOneByEmailError) => Effect.Effect<Response, never, never>
FindOneByEmailError
:
e: FindOneByEmailError
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: FindOneByEmailError
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 500 })),
type HashPasswordError: (e: HashPasswordError) => Effect.Effect<Response, never, never>
HashPasswordError
:
e: HashPasswordError
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: HashPasswordError
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 500 })),
type PasswordInvalid: (e: PasswordInvalid) => Effect.Effect<Response, never, never>
PasswordInvalid
:
e: PasswordInvalid
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: PasswordInvalid
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 400 })),
type SaveUserError: (e: SaveUserError) => Effect.Effect<Response, never, never>
SaveUserError
:
e: SaveUserError
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: SaveUserError
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 500 })),
type UserAlreadyExist: (e: UserAlreadyExist) => Effect.Effect<Response, never, never>
UserAlreadyExist
:
e: UserAlreadyExist
e
=>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const succeed: <Response>(value: Response) => Effect.Effect<Response, never, never>

Creates an Effect that succeeds with the provided value.

Use this function to represent a successful computation that yields a value of type A. The effect does not fail and does not require any environmental context.

@example

import { Effect } from "effect"

// Creating an effect that succeeds with the number 42 const success = Effect.succeed(42)

@since2.0.0

succeed
(new
var Response: new (body?: BodyInit, init?: ResponseInit) => Response
Response
(
e: UserAlreadyExist
e
.
msg: string
msg
, {
ResponseInit.status?: number
status
: 409 })),
}),
)
return
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runPromise: <Response, never>(effect: Effect.Effect<Response, never, never>, options?: {
readonly signal?: AbortSignal;
} | undefined) => Promise<Response>

Executes an effect and returns a Promise that resolves with the result.

Use runPromise when working with asynchronous effects and you need to integrate with code that uses Promises. If the effect fails, the returned Promise will be rejected with the error.

@example

import { Effect } from "effect"

// Execute an effect and handle the result with a Promise Effect.runPromise(Effect.succeed(1)).then(console.log) // Output: 1

// Execute a failing effect and handle the rejection Effect.runPromise(Effect.fail("my error")).catch((error) => { console.error("Effect failed with error:", error) })

@since2.0.0

runPromise
(
const program: Effect.Effect<Response, never, never>
program
)
}
function registerController(): Promise<Response>
registerController
()

controller เป็นคนที่เอา register() มาใช้จะเป็นคนจัดการกับ Error ที่อาจจะเกิดขึ้น ผ่าน Effect.catchTags
Tag ที่เราใส่ใน Data.TaggedError จะมีประโยชน์ตอนนี้แหละ
เมื่อเราจัดการ handle Error แล้ว ไม่ได้ return Effect.fail แล้ว Effect จะทำการตัด fail Type ออกไปให้เราด้วย
ในตัวอย่างด้านบนเมื่อเรา handle error ได้ครบ เอา mouse ไป hover ที่ program จะเห็นว่ามันมี type เป็น Effect.Effect<Response, never>
ซึ่งส่วนที่ Error เป็น never ไปแล้ว
ถ้าเราเอา program ไปรันใน Effect.runPromise มันก็จะไม่มีทาง Error เพราเราได้ handle ไปหมดแล้ว


Conclusion

ถึงตรงนี้หลายๆคนน่าจะเริ่มสนใจ Effect กันแล้ว
ผมจะเขียน blog เกี่ยวกับการใช้งาน Effect มาเรื่อยๆนะครับ
เอาเป็น blog ไปก่อนละกัน ช่วงนี้ก็ยังอัดคลิปไม่ได้ เลี้ยงลูกคนเดียวไม่สะดวกเท่าไรครับ ถ้าอัดแล้วไม่ต้องตัดมันก็จะง่ายสำหรับผมเลย แต่ถ้าไม่ตัดคลิปมันจะยาว มันก็เลยใช้พลังงานมาก เอ๊ะเหมือนบ่นให้ฟัง 😆😆

Thanks for being here ✨
ขอบคุณทุกคนที่เข้ามาอ่านจนจบนะครับ
blog นี้ค่อนข้างยาว ถ้ามีส่วนใดผิดพลาดสามารถชี้แนะกันได้นะครับ และขออภัยในความผิดพลาดนั้นล่วงหน้านะครับ

Crafted with care ✨ bycode sook logoCodeSook