การพิมพ์แบบคงที่แบบไดนามิกใน TypeScript

เผยแพร่แล้ว: 2022-03-10
สรุปอย่างย่อ ↬ ในบทความนี้ เราจะพิจารณาคุณลักษณะขั้นสูงบางอย่างของ TypeScript เช่น ชนิดยูเนี่ยน ชนิดตามเงื่อนไข ชนิดตามตัวอักษรของเทมเพลต และทั่วไป เราต้องการจัดระเบียบพฤติกรรม JavaScript แบบไดนามิกมากที่สุดในลักษณะที่เราสามารถตรวจจับจุดบกพร่องได้มากที่สุดก่อนที่จะเกิดขึ้น เราใช้การเรียนรู้หลายอย่างจากทุกบทของ TypeScript ใน 50 บทเรียน ซึ่งเป็นหนังสือที่เราเผยแพร่ที่นี่ใน Smashing Magazine ปลายปี 2020 หากคุณสนใจที่จะเรียนรู้เพิ่มเติม อย่าลืมตรวจสอบ!

JavaScript เป็นภาษาการเขียนโปรแกรมแบบไดนามิกโดยเนื้อแท้ เราในฐานะนักพัฒนาสามารถแสดงออกได้มากโดยใช้ความพยายามเพียงเล็กน้อย และภาษาและรันไทม์ของภาษานั้นก็หาคำตอบว่าเราตั้งใจจะทำอะไร นี่คือสิ่งที่ทำให้ JavaScript เป็นที่นิยมสำหรับผู้เริ่มต้น และทำให้นักพัฒนาที่มีประสบการณ์มีประสิทธิผล! อย่างไรก็ตาม มีข้อแม้: เราจำเป็นต้องตื่นตัว! ข้อผิดพลาด การพิมพ์ผิด แก้ไขพฤติกรรมของโปรแกรม: หลายอย่างเกิดขึ้นในหัวของเรา!

ลองดูตัวอย่างต่อไปนี้

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { res.status(20).send({ message: "Got you, user " + req.params.userId }); } })

เรามีเซิร์ฟเวอร์ https://expressjs.com/-style ที่ช่วยให้เราสามารถกำหนดเส้นทาง (หรือเส้นทาง) และดำเนินการเรียกกลับหากมีการร้องขอ URL

การโทรกลับใช้สองอาร์กิวเมนต์:

  1. วัตถุที่ request
    ที่นี่เราได้รับข้อมูลเกี่ยวกับวิธี HTTP ที่ใช้ (เช่น GET, POST, PUT, DELETE) และพารามิเตอร์เพิ่มเติมที่เข้ามา ในตัวอย่างนี้ userID userID มี ID ของผู้ใช้ด้วย!
  2. การ response หรือวัตถุ reply
    ที่นี่เราต้องการเตรียมการตอบสนองที่เหมาะสมจากเซิร์ฟเวอร์ไปยังไคลเอนต์ เราต้องการส่งรหัสสถานะที่ถูกต้อง ( status วิธีการ ) และส่งเอาต์พุต JSON ผ่านสาย

สิ่งที่เราเห็นในตัวอย่างนี้เรียบง่ายมาก แต่ให้ความคิดที่ดีว่าเราทำอะไรอยู่ ตัวอย่างข้างต้นยังเต็มไปด้วยข้อผิดพลาด! ดู:

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { /* Error 1 */ res.status(20).send({ /* Error 2 */ message: "Welcome, user " + req.params.userId /* Error 3 */ }); } })

โอ้ว้าว! รหัสการใช้งานสามบรรทัดและข้อผิดพลาดสามข้อ? เกิดอะไรขึ้น?

  1. ข้อผิดพลาดแรกมีความเหมาะสมยิ่ง ในขณะที่เราบอกแอปของเราว่าเราต้องการฟังคำขอ GET (ด้วยเหตุนี้ app.get ) เราจะทำบางอย่างก็ต่อเมื่อวิธีการร้องขอคือ POST ณ จุดนี้ในแอปพลิเคชันของเรา req.method ไม่สามารถ POST ได้ ดังนั้นเราจะไม่ส่งการตอบกลับใดๆ ซึ่งอาจนำไปสู่การหมดเวลาโดยไม่คาดคิด
  2. เยี่ยมมากที่เราส่งรหัสสถานะอย่างชัดเจน! 20 ไม่ใช่รหัสสถานะที่ถูกต้อง ลูกค้าอาจไม่เข้าใจว่าเกิดอะไรขึ้นที่นี่
  3. นี่คือคำตอบที่เราต้องการส่งกลับ เราเข้าถึงอาร์กิวเมนต์ที่แยกวิเคราะห์แล้ว แต่มีการพิมพ์ผิด เป็น userID ไม่ใช่ userId ผู้ใช้ของเราทุกคนจะได้รับการต้อนรับด้วย "ยินดีต้อนรับ ผู้ใช้ไม่ได้กำหนด!" สิ่งที่คุณเคยเห็นแน่นอนในป่า!

และเรื่องแบบนั้นก็เกิดขึ้น! โดยเฉพาะในจาวาสคริปต์ เราได้รับการแสดงออก – ไม่เคยต้องกังวลเกี่ยวกับประเภท – แต่ต้องให้ความสนใจอย่างใกล้ชิดกับสิ่งที่เรากำลังทำ

นี่เป็นจุดที่ JavaScript ได้รับแบ็คแลชจำนวนมากจากโปรแกรมเมอร์ที่ไม่คุ้นเคยกับภาษาโปรแกรมแบบไดนามิก พวกเขามักจะมีคอมไพเลอร์ชี้ให้เห็นปัญหาที่อาจเกิดขึ้นและจับข้อผิดพลาดล่วงหน้า พวกเขาอาจจะดูบ้าๆบอ ๆ เมื่อพวกเขาขมวดคิ้วกับงานพิเศษที่คุณต้องทำในหัวเพื่อให้แน่ใจว่าทุกอย่างถูกต้อง พวกเขาอาจบอกคุณด้วยซ้ำว่า JavaScript ไม่มีประเภท ซึ่งไม่เป็นความจริง

Anders Hejlsberg หัวหน้าสถาปนิกของ TypeScript กล่าวในประเด็นสำคัญของ MS Build 2017 ว่า “ ไม่ใช่ว่า JavaScript ไม่มีระบบประเภท ไม่มีทางที่จะทำให้เป็นทางการได้

และนี่คือจุดประสงค์หลักของ TypeScript TypeScript ต้องการทำความเข้าใจโค้ด JavaScript ของคุณดีกว่าที่คุณเข้าใจ และในกรณีที่ TypeScript ไม่เข้าใจว่าคุณหมายถึงอะไร คุณสามารถช่วยเหลือได้โดยให้ข้อมูลประเภทเพิ่มเติม

เพิ่มเติมหลังกระโดด! อ่านต่อด้านล่าง↓

การพิมพ์พื้นฐาน

และนี่คือสิ่งที่เรากำลังจะทำตอนนี้ ลองใช้วิธี get จากเซิร์ฟเวอร์แบบด่วนของเราและเพิ่มข้อมูลประเภทที่เพียงพอเพื่อให้เราสามารถแยกประเภทข้อผิดพลาดได้มากที่สุด

เราเริ่มต้นด้วยข้อมูลประเภทพื้นฐาน เรามีวัตถุ app พที่ชี้ไปที่ฟังก์ชั่น get ฟังก์ชัน get ใช้ path ซึ่งเป็นสตริงและเรียกกลับ

 const app = { get, /* post, put, delete, ... to come! */ }; function get(path: string, callback: CallbackFn) { // to be implemented --> not important right now }

ในขณะที่ string เป็นประเภทพื้นฐานที่เรียกว่า พื้นฐาน CallbackFn เป็นประเภท ผสม ที่เราต้องกำหนดไว้อย่างชัดเจน

CallbackFn เป็นประเภทฟังก์ชันที่รับสองอาร์กิวเมนต์:

  • req ซึ่งเป็นประเภท ServerRequest
  • reply ซึ่งเป็นประเภท ServerReply

CallbackFn ส่งคืน void

 type CallbackFn = (req: ServerRequest, reply: ServerReply) => void;

ServerRequest เป็นวัตถุที่ค่อนข้างซับซ้อนในกรอบงานส่วนใหญ่ เราทำเวอร์ชันย่อเพื่อวัตถุประสงค์ในการสาธิต เราส่งผ่านสตริง method สำหรับ "GET" , "POST" , "PUT" , "DELETE" ฯลฯ นอกจากนี้ยังมีบันทึก params เร็กคอร์ดคืออ็อบเจ็กต์ที่เชื่อมโยงชุดของคีย์กับชุดของคุณสมบัติ สำหรับตอนนี้ เราต้องการอนุญาตให้ทุกคีย์ string ถูกแมปกับคุณสมบัติ string เราสร้างโครงสร้างนี้ใหม่ในภายหลัง

 type ServerRequest = { method: string; params: Record<string, string>; };

สำหรับ ServerReply เราจัดวางฟังก์ชันบางอย่าง โดยรู้ว่าวัตถุ ServerReply จริงมีมากกว่านั้นมาก ฟังก์ชัน send ใช้อาร์กิวเมนต์เสริมกับข้อมูลที่เราต้องการส่ง และเรามีความเป็นไปได้ที่จะตั้งรหัสสถานะด้วยฟังก์ชัน status

 type ServerReply = { send: (obj?: any) => void; status: (statusCode: number) => ServerReply; };

มีบางอย่างอยู่แล้ว และเราสามารถแยกแยะข้อผิดพลาดสองสามข้อได้:

 app.get("/api/users/:userID", function(req, res) { if(req.method === 2) { // ^^^^^^^^^^^^^^^^^ Error, type number is not assignable to string res.status("200").send() // ^^^^^ Error, type string is not assignable to number } })

แต่เรายังคงสามารถส่งรหัสสถานะที่ไม่ถูกต้อง (หมายเลขใดก็ได้) และไม่มีเงื่อนงำเกี่ยวกับวิธีการ HTTP ที่เป็นไปได้ (สตริงใดก็ได้) มาปรับแต่งประเภทของเรากันเถอะ

ชุดเล็ก

คุณสามารถดูประเภทดั้งเดิมเป็นชุดของค่าที่เป็นไปได้ทั้งหมดของหมวดหมู่นั้น ๆ ตัวอย่างเช่น string ประกอบด้วยสตริงที่เป็นไปได้ทั้งหมดที่สามารถแสดงใน JavaScript ได้ number รวมตัวเลขที่เป็นไปได้ทั้งหมดด้วยความแม่นยำแบบ double float boolean รวมค่าบูลีนที่เป็นไปได้ทั้งหมด ซึ่งเป็นค่า true และ false

TypeScript ช่วยให้คุณปรับแต่งชุดเหล่านั้นเป็นชุดย่อยที่เล็กกว่า ตัวอย่างเช่น เราสามารถสร้าง Method ประเภทที่มีสตริงที่เป็นไปได้ทั้งหมดที่เราสามารถรับสำหรับเมธอด HTTP:

 type Methods= "GET" | "POST" | "PUT" | "DELETE"; type ServerRequest = { method: Methods; params: Record<string, string>; };

Method คือชุดที่เล็กกว่าของชุด string ที่ใหญ่กว่า Method เป็นประเภทสหภาพของประเภทตัวอักษร ชนิดตามตัวอักษรเป็นหน่วยที่เล็กที่สุดของชุดที่กำหนด สตริงตามตัวอักษร ตัวเลขตามตัวอักษร ไม่มีความคลุมเครือ ก็แค่ "GET" คุณรวมมันเข้ากับประเภทตัวอักษรอื่น ๆ เพื่อสร้างชุดย่อยของประเภทที่ใหญ่กว่าที่คุณมี คุณยังสามารถทำเซ็ตย่อยด้วยประเภทตัวอักษรของทั้ง string และ number หรือประเภทอ็อบเจ็กต์ผสมที่แตกต่างกัน มีความเป็นไปได้มากมายที่จะรวมและใส่ประเภทตามตัวอักษรลงในสหภาพแรงงาน

สิ่งนี้มีผลทันทีต่อการเรียกกลับของเซิร์ฟเวอร์ของเรา ทันใดนั้น เราสามารถแยกความแตกต่างระหว่างสี่วิธีเหล่านั้น (หรือมากกว่านั้นหากจำเป็น) และสามารถขจัดความเป็นไปได้ทั้งหมดในโค้ด TypeScript จะแนะนำเรา:

 app.get("/api/users/:userID", function (req, res) { // at this point, TypeScript knows that req.method // can take one of four possible values switch (req.method) { case "GET": break; case "POST": break; case "DELETE": break; case "PUT": break; default: // here, req.method is never req.method; } });

ด้วยคำสั่ง case ทุกอันที่คุณทำ TypeScript สามารถให้ข้อมูลเกี่ยวกับตัวเลือกที่มีให้คุณ ลองด้วยตัวคุณเอง หากคุณใช้ตัวเลือกทั้งหมดจนหมด TypeScript จะบอกคุณในสาขา default ของคุณว่าสิ่งนี้จะ never เกิดขึ้น นี่เป็นประเภทที่ never ซึ่งหมายความว่าคุณอาจถึงสถานะข้อผิดพลาดที่คุณต้องจัดการ

นั่นเป็นข้อผิดพลาดประเภทหนึ่งที่น้อยกว่า ตอนนี้เราทราบแล้วว่าวิธี HTTP ใดบ้างที่สามารถใช้ได้

เราสามารถทำเช่นเดียวกันสำหรับรหัสสถานะ HTTP โดยกำหนดชุดย่อยของตัวเลขที่ถูกต้องที่ statusCode สามารถรับได้:

 type StatusCode = 100 | 101 | 102 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 444 | 449 | 450 | 451 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 598 | 599; type ServerReply = { send: (obj?: any) => void; status: (statusCode: StatusCode) => ServerReply; };

ประเภท StatusCode เป็นประเภทสหภาพอีกครั้ง ด้วยเหตุนี้ เราจึงไม่รวมข้อผิดพลาดประเภทอื่น ทันใดนั้นรหัสแบบนั้นล้มเหลว:

 app.get("/api/user/:userID", (req, res) => { if(req.method === "POS") { // ^^^^^^^^^^^^^^^^^^^ 'Methods' and '"POS"' have no overlap. res.status(20) // ^^ '20' is not assignable to parameter of type 'StatusCode' } })
และซอฟต์แวร์ของเราก็ปลอดภัยขึ้นมาก! แต่เราสามารถทำได้มากขึ้น!

ใส่ Generics

เมื่อเรากำหนดเส้นทางด้วย app.get เราทราบโดยปริยายว่าวิธี HTTP เดียวที่เป็นไปได้คือ "GET" แต่ด้วยคำจำกัดความประเภทของเรา เรายังต้องตรวจสอบส่วนที่เป็นไปได้ทั้งหมดของสหภาพแรงงาน

ประเภทของ CallbackFn นั้นถูกต้อง เนื่องจากเราสามารถกำหนดฟังก์ชันการโทรกลับสำหรับวิธี HTTP ที่เป็นไปได้ทั้งหมด แต่ถ้าเราเรียก app.get อย่างชัดแจ้ง จะเป็นการดีที่จะบันทึกขั้นตอนพิเศษบางอย่างที่จำเป็นเพื่อให้สอดคล้องกับการพิมพ์เท่านั้น

ข้อมูลทั่วไปของ TypeScript สามารถช่วยได้! Generics เป็นหนึ่งในคุณสมบัติหลักใน TypeScript ที่ช่วยให้คุณได้รับพฤติกรรมแบบไดนามิกมากที่สุดจากประเภทคงที่ ใน TypeScript ใน 50 บทเรียน เราใช้สามบทสุดท้ายในการขุดเจาะลึกถึงความซับซ้อนทั้งหมดของยาสามัญและฟังก์ชันการทำงานที่เป็นเอกลักษณ์

สิ่งที่คุณต้องรู้ในตอนนี้คือ เราต้องการกำหนด ServerRequest ในลักษณะที่เราสามารถระบุส่วนหนึ่งของ Methods แทนที่จะเป็นทั้งชุด สำหรับสิ่งนั้น เราใช้ไวยากรณ์ทั่วไป ซึ่งเราสามารถกำหนดพารามิเตอร์ได้เหมือนกับที่เราจะทำกับฟังก์ชัน:

 type ServerRequest<Met extends Methods> = { method: Met; params: Record<string, string>; };

นี่คือสิ่งที่เกิดขึ้น:

  1. ServerRequest กลายเป็นประเภททั่วไปตามที่ระบุโดยวงเล็บมุม
  2. เรากำหนดพารามิเตอร์ทั่วไปที่เรียกว่า Met ซึ่งเป็นชุดย่อยของประเภท Methods
  3. เราใช้พารามิเตอร์ทั่วไปนี้เป็นตัวแปรทั่วไปเพื่อกำหนดวิธีการ

ฉันยังสนับสนุนให้คุณดูบทความของฉันเกี่ยวกับการตั้งชื่อพารามิเตอร์ทั่วไป

ด้วยการเปลี่ยนแปลงดังกล่าว เราสามารถระบุ ServerRequest ต่างๆ โดยไม่ต้องทำซ้ำ:

 type OnlyGET = ServerRequest<"GET">; type OnlyPOST = ServerRequest<"POST">; type POSTorPUT = ServerRquest<"POST" | "PUT">;

เนื่องจากเราเปลี่ยนอินเทอร์เฟซของ ServerRequest เราจึงต้องเปลี่ยนแปลงประเภทอื่นๆ ทั้งหมดที่ใช้ ServerRequest เช่น CallbackFn และฟังก์ชัน get :

 type CallbackFn<Met extends Methods> = ( req: ServerRequest<Met>, reply: ServerReply ) => void; function get(path: string, callback: CallbackFn<"GET">) { // to be implemented }

ด้วยฟังก์ชัน get เราจะส่งต่ออาร์กิวเมนต์ที่แท้จริงไปยังประเภททั่วไปของเรา เรารู้ว่านี่จะไม่ใช่แค่ส่วนย่อยของ Methods เรารู้แน่ชัดว่าเรากำลังจัดการกับส่วนย่อยใด

ตอนนี้ เมื่อเราใช้ app.get เรามีค่าที่เป็นไปได้สำหรับ req.method :

 app.get("/api/users/:userID", function (req, res) { req.method; // can only be get });

เพื่อให้แน่ใจว่าเราไม่ถือว่าวิธีการ HTTP เช่น "POST" หรือคล้ายกันนั้นพร้อมใช้งานเมื่อเราสร้างการเรียกกลับของ app.get เรารู้แน่ชัดว่าเรากำลังเผชิญกับอะไร ณ จุดนี้ ดังนั้นลองพิจารณาสิ่งนั้นในประเภทของเรา

เราได้ทำหลายอย่างแล้วเพื่อให้แน่ใจว่า request.method มีการพิมพ์ที่สมเหตุสมผลและแสดงถึงสถานการณ์จริง ข้อดีอย่างหนึ่งที่เราได้รับจากการตั้งค่าย่อยของ Methods union type คือ เราสามารถสร้างฟังก์ชันเรียกกลับเพื่อวัตถุประสงค์ทั่วไป ภายนอก app.get ที่พิมพ์ได้อย่างปลอดภัย:

 const handler: CallbackFn<"PUT" | "POST"> = function(res, req) { res.method // can be "POST" or "PUT" }; const handlerForAllMethods: CallbackFn<Methods> = function(res, req) { res.method // can be all methods }; app.get("/api", handler); // ^^^^^^^ Nope, we don't handle "GET" app.get("/api", handlerForAllMethods); // This works

พิมพ์ Params

สิ่งที่เรายังไม่ได้สัมผัสคือการพิมพ์วัตถุ params จนถึงตอนนี้ เราได้รับบันทึกที่อนุญาตให้เข้าถึงคีย์ string ทุกอัน ตอนนี้เป็นหน้าที่ของเราที่จะต้องทำให้เฉพาะเจาะจงมากขึ้นอีกนิด!

เราทำได้โดยการเพิ่มตัวแปรทั่วไปอื่น หนึ่งสำหรับวิธีการ หนึ่งสำหรับคีย์ที่เป็นไปได้ใน Record ของเรา :

 type ServerRequest<Met extends Methods, Par extends string = string> = { method: Met; params: Record<Par, string>; };

ตัวแปรประเภททั่วไป Par สามารถเป็นชุดย่อยของประเภท string และค่าดีฟอลต์คือทุกสตริง ด้วยเหตุนี้ เราจึงสามารถบอก ServerRequest ได้ว่าคีย์ใดที่เราคาดหวัง:

 // request.method = "GET" // request.params = { // userID: string // } type WithUserID = ServerRequest<"GET", "userID">

มาเพิ่มอาร์กิวเมนต์ใหม่ให้กับฟังก์ชัน get และประเภท CallbackFn เพื่อให้เราสามารถตั้งค่าพารามิเตอร์ที่ร้องขอได้:

 function get<Par extends string = string>( path: string, callback: CallbackFn<"GET", Par> ) { // to be implemented } type CallbackFn<Met extends Methods, Par extends string> = ( req: ServerRequest<Met, Par>, reply: ServerReply ) => void;

หากเราไม่ได้ตั้งค่า Par ไว้อย่างชัดเจน ชนิดนั้นจะทำงานเหมือนที่เราคุ้นเคย เนื่องจากค่าเริ่มต้น Par จะเป็น string ถ้าเราตั้งค่าไว้ เราก็มีคำจำกัดความที่เหมาะสมสำหรับวัตถุ req.params ทันที!

 app.get<"userID">("/api/users/:userID", function (req, res) { req.params.userID; // Works!! req.params.anythingElse; // doesn't work!! });

ที่ที่ดี! มีสิ่งเล็กน้อยที่สามารถปรับปรุงได้ เรายังคงสามารถส่ง ทุก สตริงไปยังอาร์กิวเมนต์ path ของ app.get จะดีกว่าไหมถ้าเราสามารถสะท้อน Par ในนั้นด้วย?

เราทำได้! ด้วยการเปิดตัวเวอร์ชัน 4.1 TypeScript สามารถสร้าง เทมเพลตประเภทตัวอักษร ได้ ในทางวากยสัมพันธ์ พวกมันทำงานเหมือนกับตัวอักษรเทมเพลตสตริง แต่ในระดับประเภท ที่ซึ่งเราสามารถแยก set string ออกเป็น subsets ด้วย string literal types (เช่นที่เราทำกับ Methods) ชนิด template literal ช่วยให้เราสามารถรวมสเปกตรัมของสตริงทั้งหมดได้

มาสร้างประเภทที่เรียกว่า IncludesRouteParams ซึ่งเราต้องการให้แน่ใจว่า Par ถูกรวมอย่างถูกต้องในรูปแบบ Express ในการเพิ่มโคลอนหน้าชื่อพารามิเตอร์:

 type IncludesRouteParams<Par extends string> = | `${string}/:${Par}` | `${string}/:${Par}/${string}`;

ประเภททั่วไป IncludesRouteParams รับหนึ่งอาร์กิวเมนต์ ซึ่งเป็นชุดย่อยของ string มันสร้างประเภทยูเนี่ยนของตัวอักษรเทมเพลตสองตัว:

  1. เทมเพลตแรกตามตัวอักษรเริ่มต้นด้วย string ใดๆ จากนั้นรวม a / อักขระตามด้วยอักขระ : ตามด้วยชื่อพารามิเตอร์ ซึ่งทำให้แน่ใจได้ว่าเราจับทุกกรณีที่พารามิเตอร์อยู่ที่ส่วนท้ายของสตริงเส้นทาง
  2. เทมเพลตตัวอักษรที่สองเริ่มต้นด้วย string ใดๆ ตามด้วยรูปแบบเดียวกันของ / , : และชื่อพารามิเตอร์ จากนั้นเราก็มีอักขระอื่น / ตามด้วยสตริง ใด ๆ สาขาของประเภทสหภาพนี้ช่วยให้แน่ใจว่าเราตรวจจับทุกกรณีที่มีพารามิเตอร์อยู่ที่ไหนสักแห่งภายในเส้นทาง

นี่คือลักษณะที่ IncludesRouteParams ที่มีชื่อพารามิเตอร์ userID ทำงานด้วยกรณีทดสอบที่แตกต่างกัน:

 const a: IncludeRouteParams<"userID"> = "/api/user/:userID" // const a: IncludeRouteParams<"userID"> = "/api/user/:userID/orders" // const a: IncludeRouteParams<"userID"> = "/api/user/:userId" // const a: IncludeRouteParams<"userID"> = "/api/user" // const a: IncludeRouteParams<"userID"> = "/api/user/:userIDAndmore" //

มารวมประเภทยูทิลิตี้ใหม่ของเราในการประกาศฟังก์ชัน get

 function get<Par extends string = string>( path: IncludesRouteParams<Par>, callback: CallbackFn<"GET", Par> ) { // to be implemented } app.get<"userID">( "/api/users/:userID", function (req, res) { req.params.userID; // YEAH! } );

ยอดเยี่ยม! เราได้รับกลไกความปลอดภัยอื่นเพื่อให้แน่ใจว่าเราจะไม่พลาดการเพิ่มพารามิเตอร์ในเส้นทางจริง! แรงแค่ไหน.

การผูกมัดทั่วไป

แต่เดาว่าฉันยังคงไม่พอใจกับมัน แนวทางดังกล่าวมีปัญหาบางประการที่เห็นได้ชัดในทันทีที่เส้นทางของคุณซับซ้อนขึ้นเล็กน้อย

  1. ปัญหาแรกที่ฉันมีคือ เราจำเป็นต้องระบุพารามิเตอร์ของเราอย่างชัดเจนในพารามิเตอร์ประเภททั่วไป เราต้องผูก Par กับ "userID" แม้ว่าเราจะระบุมันในอาร์กิวเมนต์เส้นทางของฟังก์ชันก็ตาม นี่ไม่ใช่ JavaScript-y!
  2. วิธีการนี้จัดการพารามิเตอร์เส้นทางเดียวเท่านั้น ช่วงเวลาที่เราเพิ่มสหภาพ เช่น "userID" | "orderId" "userID" | "orderId" การตรวจสอบที่ล้มเหลวนั้นพอใจโดยมีเพียง หนึ่ง ในอาร์กิวเมนต์ที่มีอยู่ นั่นเป็นวิธีที่ชุดทำงาน อาจเป็นอย่างใดอย่างหนึ่ง

จะต้องมีวิธีที่ดีกว่า และมี มิฉะนั้น บทความนี้จะจบลงด้วยความขมขื่น

มาผกผันคำสั่งกันเถอะ! อย่าพยายามกำหนดพารามิเตอร์ของเส้นทางในตัวแปรประเภททั่วไป แต่ให้แยกตัวแปรออกจาก path ที่เราส่งผ่านเป็นอาร์กิวเมนต์แรกของ app.get

เพื่อให้ได้ค่าที่แท้จริง เราต้องดูว่าการ รวมทั่วไป ทำงานอย่างไรใน TypeScript ลองใช้ฟังก์ชัน identity นี้เช่น:

 function identity<T>(inp: T) : T { return inp }

อาจเป็นฟังก์ชันทั่วไปที่น่าเบื่อที่สุดที่คุณเคยเห็น แต่แสดงให้เห็นจุดหนึ่งได้อย่างสมบูรณ์แบบ identity รับหนึ่งอาร์กิวเมนต์ และส่งคืนอินพุตเดิมอีกครั้ง type เป็นประเภททั่วไป T และส่งคืนประเภทเดียวกันด้วย

ตอนนี้เราสามารถผูก T กับ string ได้เช่น:

 const z = identity<string>("yes"); // z is of type string

การเชื่อมโยงทั่วไปอย่างชัดแจ้งนี้ทำให้แน่ใจว่าเราส่งผ่าน strings ไปยัง identity เท่านั้น และเนื่องจากเราเชื่อมโยงอย่างชัดเจน ประเภทการส่งคืนจึงเป็น string ด้วย ถ้าเราลืมผูก สิ่งที่น่าสนใจจะเกิดขึ้น:

 const y = identity("yes") // y is of type "yes"

ในกรณีนั้น TypeScript จะอนุมานประเภทจากอาร์กิวเมนต์ที่คุณส่งผ่าน และผูก T กับ สตริงตามตัวอักษรประเภท "yes" นี่เป็นวิธีที่ยอดเยี่ยมในการแปลงอาร์กิวเมนต์ของฟังก์ชันเป็นประเภทลิเทอรัล ซึ่งเราจะใช้ในประเภททั่วไปอื่นๆ ของเรา

มาทำกันโดยปรับ app.get

 function get<Path extends string = string>( path: Path, callback: CallbackFn<"GET", ParseRouteParams<Path>> ) { // to be implemented }

เราลบประเภท Par ทั่วไปและเพิ่ม Path Path สามารถเป็นส่วนย่อยของ string ใดก็ได้ เราตั้งค่า path ไปยังประเภททั่วไปนี้ Path ซึ่งหมายความว่าเมื่อเราส่งพารามิเตอร์เพื่อ get เราจะตรวจจับประเภทตัวอักษรของสตริงได้ เราส่งต่อ Path ไปยัง ParseRouteParams ชนิดทั่วไปที่เรายังไม่ได้สร้าง

มาทำงานกับ ParseRouteParams ที่นี่เราเปลี่ยนลำดับเหตุการณ์อีกครั้ง แทนที่จะส่งพารามิเตอร์เส้นทางที่ร้องขอไปยังส่วนทั่วไปเพื่อให้แน่ใจว่าเส้นทางนั้นถูกต้อง เราจะส่งเส้นทางเส้นทางและแยกพารามิเตอร์เส้นทางที่เป็นไปได้ เพื่อที่เราต้องสร้างประเภทเงื่อนไข

ประเภทตามเงื่อนไขและประเภทตามตัวอักษรของเทมเพลตแบบเรียกซ้ำ

ประเภทเงื่อนไขจะคล้ายกับตัวดำเนินการ ternary ใน JavaScript คุณตรวจสอบเงื่อนไข และหากตรงตามเงื่อนไข คุณจะส่งคืนสาขา A มิฉะนั้น คุณจะส่งคืนสาขา B ตัวอย่างเช่น

 type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}` ? P : never;

ที่นี่เราตรวจสอบว่า Rte เป็นชุดย่อยของทุกเส้นทางที่ลงท้ายด้วยพารามิเตอร์ที่ท้าย Express-style หรือไม่ (โดยมี "/:" นำหน้า) ถ้าใช่ เราอนุมานสตริงนี้ ซึ่งหมายความว่าเราจับเนื้อหาเป็นตัวแปรใหม่ หากตรงตามเงื่อนไข เราจะคืนค่าสตริงที่ดึงออกมาใหม่ มิฉะนั้น เราจะไม่ส่งคืน เช่น "ไม่มีพารามิเตอร์เส้นทาง"

ถ้าเราลองใช้งาน เราจะได้อะไรดังนี้:

 type Params = ParseRouteParams<"/api/user/:userID"> // Params is "userID" type NoParams = ParseRouteParams<"/api/user"> // NoParams is never --> no params!

เยี่ยม มันดีกว่าที่เราทำก่อนหน้านี้มาก ตอนนี้ เราต้องการจับพารามิเตอร์ที่เป็นไปได้อื่นๆ ทั้งหมด เพื่อที่เราต้องเพิ่มเงื่อนไขอื่น:

 type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}/${infer Rest}` ? P | ParseRouteParams<`/${Rest}`> : Rte extends `${string}/:${infer P}` ? P : never;

ประเภทเงื่อนไขของเราทำงานดังนี้:

  1. ในเงื่อนไขแรก เราจะตรวจสอบว่ามีพารามิเตอร์เส้นทางอยู่ระหว่างเส้นทางหรือไม่ หากเป็นเช่นนั้น เราจะแยกทั้งพารามิเตอร์เส้นทางและทุกอย่างที่มาหลังจากนั้น เราส่งคืนพารามิเตอร์เส้นทางที่เพิ่งพบ P ในสหภาพที่เราเรียกประเภททั่วไปแบบเรียกซ้ำกับส่วนที่ Rest ตัวอย่างเช่น หากเราส่งเส้นทาง "/api/users/:userID/orders/:orderID" ไปยัง ParseRouteParams เราจะอนุมาน "userID" ลงใน P และ "orders/:orderID" ลงใน Rest เราเรียกประเภทเดียวกันกับ Rest
  2. นี่คือที่มาของเงื่อนไขที่สอง ที่นี่เราจะตรวจสอบว่ามีประเภทอยู่ที่ส่วนท้ายหรือไม่ นี่เป็นกรณีของ "orders/:orderID" เราแยก "orderID" และส่งคืนประเภทตัวอักษรนี้
  3. หากไม่มีพารามิเตอร์เส้นทางเหลือ เราจะไม่ส่งคืน

Dan Vanderkam แสดงประเภทที่คล้ายกันและซับซ้อนกว่าสำหรับ ParseRouteParams แต่ประเภทที่คุณเห็นด้านบนน่าจะใช้ได้เช่นกัน หากเราลองใช้ ParseRouteParams ที่ปรับปรุงใหม่ เราจะได้สิ่งนี้:

 // Params is "userID" type Params = ParseRouteParams<"/api/user/:userID"> // MoreParams is "userID" | "orderID" type MoreParams = ParseRouteParams<"/api/user/:userID/orders/:orderId">

ลองใช้รูปแบบใหม่นี้และดูว่าการใช้งาน app.get ครั้งสุดท้ายของเราเป็นอย่างไร

 app.get("/api/users/:userID/orders/:orderID", function (req, res) { req.params.userID; // YES!! req.params.orderID; // Also YES!!! });

ว้าว. ดูเหมือนว่าโค้ด JavaScript ที่เรามีในตอนเริ่มต้น!

ประเภทคงที่สำหรับพฤติกรรมแบบไดนามิก

ประเภทที่เราเพิ่งสร้างขึ้นสำหรับหนึ่งฟังก์ชัน app.get ตรวจสอบให้แน่ใจว่าเราได้แยกข้อผิดพลาดที่เป็นไปได้มากมาย:

  1. เราสามารถส่งรหัสสถานะที่เป็นตัวเลขที่เหมาะสมไปยัง res.status()
  2. req.method เป็นหนึ่งในสี่สตริงที่เป็นไปได้ และเมื่อเราใช้ app.get เรารู้ว่ามันเป็น "GET" เท่านั้น
  3. เราสามารถแยกวิเคราะห์พารามิเตอร์เส้นทาง และตรวจสอบให้แน่ใจว่าไม่มีการพิมพ์ผิดใดๆ ในการโทรกลับของเรา

หากเราดูตัวอย่างตั้งแต่ต้นบทความนี้ เราได้รับข้อความแสดงข้อผิดพลาดต่อไปนี้:

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { // ^^^^^^^^^^^^^^^^^^^^^ // This condition will always return 'false' // since the types '"GET"' and '"POST"' have no overlap. res.status(20).send({ // ^^ // Argument of type '20' is not assignable to // parameter of type 'StatusCode' message: "Welcome, user " + req.params.userId // ^^^^^^ // Property 'userId' does not exist on type // '{ userID: string; }'. Did you mean 'userID'? }); } })

และทั้งหมดนั้นก่อนที่เราจะรันโค้ดของเรา! เซิร์ฟเวอร์แบบด่วนเป็นตัวอย่างที่สมบูรณ์แบบของลักษณะไดนามิกของ JavaScript ขึ้นอยู่กับวิธีการที่คุณเรียกใช้ สตริงที่คุณส่งผ่านสำหรับอาร์กิวเมนต์แรก การเปลี่ยนแปลงพฤติกรรมมากมายภายในการโทรกลับ ยกตัวอย่างอื่น และทุกประเภทของคุณดูแตกต่างไปจากเดิมอย่างสิ้นเชิง

แต่ด้วยประเภทที่กำหนดไว้อย่างดีสองสามประเภท เราสามารถตรวจจับพฤติกรรมแบบไดนามิกนี้ขณะแก้ไขโค้ดของเรา ณ เวลาคอมไพล์ด้วยประเภทสแตติก ไม่ใช่ขณะรันไทม์เมื่อสิ่งต่าง ๆ บูม!

และนี่คือพลังของ TypeScript ระบบประเภทสแตติกที่พยายามจัดระเบียบพฤติกรรม JavaScript แบบไดนามิกทั้งหมดที่เราทุกคนรู้ดีเป็นอย่างดี ถ้าคุณต้องการลองใช้ตัวอย่างที่เราเพิ่งสร้างขึ้น ตรงไปที่สนามเด็กเล่น TypeScript และเล่นซอกับมัน


TypeScript ใน 50 บทเรียนโดย Stefan Baumgartner ในบทความนี้ เราได้กล่าวถึงแนวคิดมากมาย หากคุณต้องการทราบข้อมูลเพิ่มเติม โปรดดูที่ TypeScript ใน 50 บทเรียน ซึ่งคุณจะได้รับคำแนะนำเบื้องต้นเกี่ยวกับระบบการพิมพ์ในบทเรียนเล็กๆ ที่เข้าใจง่าย เวอร์ชัน Ebook จะพร้อมใช้งานทันที และหนังสือที่พิมพ์จะเป็นข้อมูลอ้างอิงที่ดีสำหรับไลบรารีการเข้ารหัสของคุณ