ข้ามไปยังเนื้อหา

Type System

เราเรียก schema ว่า “contract” มาหลายครั้งแล้ว ตอนนี้มาเปิดดูข้างในกัน schema สร้างจากชุด type kind เล็ก ๆ ที่เขียนด้วย Schema Definition Language (SDL) เรียนรู้ building block เหล่านี้แล้วคุณจะอ่าน — และออกแบบ — GraphQL API ตัวไหนก็ได้

type kind ที่พบบ่อยที่สุดคือ object type: ชุดของ field ที่มีชื่อ แต่ละ field มีชื่อและ type และอาจรับ argument ได้ ด้านล่างคือ object type ที่อธิบาย track ในไลบรารีเพลง:

type Track {
id: ID!
title: String!
durationSeconds: Int!
explicit: Boolean
artist: Artist!
}

Track มีห้า field type ของแต่ละ field บอก client ว่าจะได้อะไรกลับมา ทั้ง ID หนึ่งตัว ค่า String และ Int บางตัว Boolean หนึ่งตัว และลิงก์ไปยัง object type อีกตัวคือ Artist field สุดท้ายนี่แหละที่เปลี่ยน record แบน ๆ ให้กลายเป็น graph เพราะ field ชี้ไปหา type อื่นได้ และ client ก็เดินผ่านไปต่อได้

การเลือกทุกครั้งสุดท้ายจะลงเอยที่ scalar: ค่าเดี่ยวที่แบ่งย่อยไม่ได้ GraphQL มาพร้อม built-in scalar ห้าตัว:

  • Int — จำนวนเต็มแบบมีเครื่องหมายขนาด 32 บิต
  • Float — ค่าทศนิยมแบบ double-precision ที่มีเครื่องหมาย
  • String — ข้อความ UTF-8
  • Booleantrue หรือ false
  • ID — ตัวระบุที่ไม่ซ้ำ serialize เป็น string แต่เชิงความหมายเป็นแบบทึบ (ไม่ควรเอาไปคำนวณ)

คุณยังสามารถนิยาม custom scalar ได้ (เช่น DateTime หรือ EmailAddress) เพื่อแนบกฎการ validate และ serialize แต่ทั้งห้าตัวข้างต้นคือรากฐาน

object type สามตัวพิเศษกว่าตัวอื่น เพราะเป็น จุดเข้า สู่ graph query ต้องเริ่มจากที่ไหนสักแห่ง และสามชื่อนี้คือจุดเริ่ม:

  • Query — จุดเข้าสำหรับการอ่าน การอ่านทุกครั้งเริ่มด้วยการเลือก field ที่นี่
  • Mutation — จุดเข้าสำหรับการเขียน field ที่นี่สร้าง อัปเดต หรือลบข้อมูล
  • Subscription — จุดเข้าสำหรับ real-time field ที่นี่ stream การอัปเดตตามเวลา

มีเพียง Query เท่านั้นที่จำเป็น schema ที่มีการอ่านแต่ไม่มีการเขียนก็แค่ละ Mutation และ Subscription ออกไป

flowchart TD
  Query["Query (read entry)"]
  Mutation["Mutation (write entry)"]
  Subscription["Subscription (stream entry)"]
  Track["type Track"]
  Artist["type Artist"]
  Scalars["Scalars: ID, String, Int, Boolean, Float"]
  Query --> Track
  Mutation --> Track
  Track -->|"artist field"| Artist
  Track --> Scalars
  Artist --> Scalars
root type คือประตูทางเข้า; object type และ scalar ประกอบกันเป็นส่วนที่เหลือของ graph

โดยค่าเริ่มต้น ทุก field ใน GraphQL เป็น nullable คือจะคืนค่าหรือคืน null ก็ได้ เติม ! ต่อท้ายเมื่อไหร่ field ก็กลายเป็น non-null คือ server รับประกันว่ามีค่าจริง ไม่มีวันเป็น null ให้อ่าน ! ว่า “รับประกัน”:

type Artist {
id: ID! # always present
name: String! # always present
bio: String # may be null — biography is optional
tracks: [Track!]! # a non-null list of non-null Tracks
}

บรรทัดสุดท้ายนั้นคุ้มค่าที่จะถอดความ [Track!]! คือ list type ที่มีเครื่องหมายอัศเจรีย์สองตัวทำงานสองอย่างต่างกัน:

  • Track! ตัวในหมายความว่าทุก element ในลิสต์เป็น Track แบบ non-null
  • ! ตัวนอกแปลว่าตัว list เองไม่มีวันเป็น null (แต่ยังว่าง [] ได้)

ดังนั้น tracks คืน list เสมอ และใน list นั้นไม่มีวันมีรู null nullability เป็นส่วนหนึ่งของ contract เพราะบอก client ชัด ๆ ว่า field ไหนต้องระวัง และ field ไหนไว้ใจได้

ตัวอย่างด้านล่างนิยาม schema สอง type — Artist กับ Track — ต่อ resolver แล้วรัน query ที่ข้ามจาก type หนึ่งไปอีก type หนึ่ง กด Run เพื่อรันด้วย GraphQL engine จริง

JavaScript

ดูผลลัพธ์ใกล้ ๆ title กับ durationSeconds กลับมาเป็นค่าที่รับประกันได้ เพราะประกาศเป็น non-null ใน schema ส่วน artist เป็น object ที่ซ้อนอยู่ — query ก้าวจาก Track เข้าไปใน Artist — และข้างในนั้น bio กลับมาเป็น null ซึ่งทำได้ เพราะ bio ประกาศไว้โดยไม่มี ! type system ทำนายรูปร่างทั้งหมดนี้ได้ตั้งแต่ก่อน resolver จะรันด้วยซ้ำ

ข้อดีข้อแลกเปลี่ยน
validate query ก่อน runtime — error เจอตั้งแต่ developmenttype system เพิ่ม verbosity ใน schema definition
auto-complete และ type checking ใน IDEschema migration ต้องระวัง backward compatibility
code generation สำหรับ TypeScript type จาก schemanullable by default ทำให้ต้อง null check ที่ client
introspection ทำให้ tooling และ playground ทำงานได้type ที่ซับซ้อนทำให้ schema อ่านยากขึ้น

Nullable ทุก field โดยไม่คิด อาการ:

  • schema มี String แทนที่จะเป็น String! ทุกที่
  • client ต้อง null check ทุก field แม้ field นั้นจะไม่มีทางเป็น null จริง ๆ
  • ออกแบบ nullability อย่างตั้งใจ — ! เมื่อมั่นใจ, nullable เมื่อ field อาจไม่มีค่า

Schema สะท้อน Database Table ตรง ๆ อาการ:

  • type ใน schema ชื่อเดียวกับ table ใน database
  • field ใน schema = column ใน database ทุกตัว
  • expose implementation detail ให้ client โดยไม่จำเป็น

💡 ตัวอย่างจากของจริง

GitHub GraphQL API:

  • ใช้ custom scalar เช่น URI, DateTime, HTML นอกเหนือจาก built-in
  • type system ทำให้ client รู้ว่า field ไหน return URL, รูปแบบวันที่, หรือ HTML string

Shopify:

  • ใช้ Interface สำหรับ Node (ทุก object ที่มี id) เพื่อให้ cache ง่าย
  • ใช้ Union สำหรับ search result ที่อาจเป็น Product, Collection, หรือ Page
เครื่องหมายอัศเจรีย์ที่ต่อท้าย อย่างใน String! ประกาศอะไรเกี่ยวกับ field?
ข้อใดต่อไปนี้ที่ไม่ใช่หนึ่งใน built-in scalar type ทั้งห้าของ GraphQL?
root type ใดที่เป็นจุดเข้าสำหรับการอ่าน และเป็นตัวเดียวที่ schema ต้องนิยาม?
type [Track!]! หมายความว่าอย่างไร?