การพิมพ์แบบคงที่แบบไดนามิกใน TypeScript
เผยแพร่แล้ว: 2022-03-10JavaScript เป็นภาษาการเขียนโปรแกรมแบบไดนามิกโดยเนื้อแท้ เราในฐานะนักพัฒนาสามารถแสดงออกได้มากโดยใช้ความพยายามเพียงเล็กน้อย และภาษาและรันไทม์ของภาษานั้นก็หาคำตอบว่าเราตั้งใจจะทำอะไร นี่คือสิ่งที่ทำให้ 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
การโทรกลับใช้สองอาร์กิวเมนต์:
- วัตถุที่
request
ที่นี่เราได้รับข้อมูลเกี่ยวกับวิธี HTTP ที่ใช้ (เช่น GET, POST, PUT, DELETE) และพารามิเตอร์เพิ่มเติมที่เข้ามา ในตัวอย่างนี้userID
userID
มี ID ของผู้ใช้ด้วย! - การ
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 */ }); } })
โอ้ว้าว! รหัสการใช้งานสามบรรทัดและข้อผิดพลาดสามข้อ? เกิดอะไรขึ้น?
- ข้อผิดพลาดแรกมีความเหมาะสมยิ่ง ในขณะที่เราบอกแอปของเราว่าเราต้องการฟังคำขอ GET (ด้วยเหตุนี้
app.get
) เราจะทำบางอย่างก็ต่อเมื่อวิธีการร้องขอคือ POST ณ จุดนี้ในแอปพลิเคชันของเราreq.method
ไม่สามารถ POST ได้ ดังนั้นเราจะไม่ส่งการตอบกลับใดๆ ซึ่งอาจนำไปสู่การหมดเวลาโดยไม่คาดคิด - เยี่ยมมากที่เราส่งรหัสสถานะอย่างชัดเจน!
20
ไม่ใช่รหัสสถานะที่ถูกต้อง ลูกค้าอาจไม่เข้าใจว่าเกิดอะไรขึ้นที่นี่ - นี่คือคำตอบที่เราต้องการส่งกลับ เราเข้าถึงอาร์กิวเมนต์ที่แยกวิเคราะห์แล้ว แต่มีการพิมพ์ผิด เป็น
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>; };
นี่คือสิ่งที่เกิดขึ้น:
-
ServerRequest
กลายเป็นประเภททั่วไปตามที่ระบุโดยวงเล็บมุม - เรากำหนดพารามิเตอร์ทั่วไปที่เรียกว่า
Met
ซึ่งเป็นชุดย่อยของประเภทMethods
- เราใช้พารามิเตอร์ทั่วไปนี้เป็นตัวแปรทั่วไปเพื่อกำหนดวิธีการ
ฉันยังสนับสนุนให้คุณดูบทความของฉันเกี่ยวกับการตั้งชื่อพารามิเตอร์ทั่วไป
ด้วยการเปลี่ยนแปลงดังกล่าว เราสามารถระบุ 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
มันสร้างประเภทยูเนี่ยนของตัวอักษรเทมเพลตสองตัว:
- เทมเพลตแรกตามตัวอักษรเริ่มต้นด้วย
string
ใดๆ จากนั้นรวม a/
อักขระตามด้วยอักขระ:
ตามด้วยชื่อพารามิเตอร์ ซึ่งทำให้แน่ใจได้ว่าเราจับทุกกรณีที่พารามิเตอร์อยู่ที่ส่วนท้ายของสตริงเส้นทาง - เทมเพลตตัวอักษรที่สองเริ่มต้นด้วย
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! } );
ยอดเยี่ยม! เราได้รับกลไกความปลอดภัยอื่นเพื่อให้แน่ใจว่าเราจะไม่พลาดการเพิ่มพารามิเตอร์ในเส้นทางจริง! แรงแค่ไหน.
การผูกมัดทั่วไป
แต่เดาว่าฉันยังคงไม่พอใจกับมัน แนวทางดังกล่าวมีปัญหาบางประการที่เห็นได้ชัดในทันทีที่เส้นทางของคุณซับซ้อนขึ้นเล็กน้อย
- ปัญหาแรกที่ฉันมีคือ เราจำเป็นต้องระบุพารามิเตอร์ของเราอย่างชัดเจนในพารามิเตอร์ประเภททั่วไป เราต้องผูก
Par
กับ"userID"
แม้ว่าเราจะระบุมันในอาร์กิวเมนต์เส้นทางของฟังก์ชันก็ตาม นี่ไม่ใช่ JavaScript-y! - วิธีการนี้จัดการพารามิเตอร์เส้นทางเดียวเท่านั้น ช่วงเวลาที่เราเพิ่มสหภาพ เช่น
"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;
ประเภทเงื่อนไขของเราทำงานดังนี้:
- ในเงื่อนไขแรก เราจะตรวจสอบว่ามีพารามิเตอร์เส้นทางอยู่ระหว่างเส้นทางหรือไม่ หากเป็นเช่นนั้น เราจะแยกทั้งพารามิเตอร์เส้นทางและทุกอย่างที่มาหลังจากนั้น เราส่งคืนพารามิเตอร์เส้นทางที่เพิ่งพบ
P
ในสหภาพที่เราเรียกประเภททั่วไปแบบเรียกซ้ำกับส่วนที่Rest
ตัวอย่างเช่น หากเราส่งเส้นทาง"/api/users/:userID/orders/:orderID"
ไปยังParseRouteParams
เราจะอนุมาน"userID"
ลงในP
และ"orders/:orderID"
ลงในRest
เราเรียกประเภทเดียวกันกับRest
- นี่คือที่มาของเงื่อนไขที่สอง ที่นี่เราจะตรวจสอบว่ามีประเภทอยู่ที่ส่วนท้ายหรือไม่ นี่เป็นกรณีของ
"orders/:orderID"
เราแยก"orderID"
และส่งคืนประเภทตัวอักษรนี้ - หากไม่มีพารามิเตอร์เส้นทางเหลือ เราจะไม่ส่งคืน
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
ตรวจสอบให้แน่ใจว่าเราได้แยกข้อผิดพลาดที่เป็นไปได้มากมาย:
- เราสามารถส่งรหัสสถานะที่เป็นตัวเลขที่เหมาะสมไปยัง
res.status()
-
req.method
เป็นหนึ่งในสี่สตริงที่เป็นไปได้ และเมื่อเราใช้app.get
เรารู้ว่ามันเป็น"GET"
เท่านั้น - เราสามารถแยกวิเคราะห์พารามิเตอร์เส้นทาง และตรวจสอบให้แน่ใจว่าไม่มีการพิมพ์ผิดใดๆ ในการโทรกลับของเรา
หากเราดูตัวอย่างตั้งแต่ต้นบทความนี้ เราได้รับข้อความแสดงข้อผิดพลาดต่อไปนี้:
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 บทเรียน ซึ่งคุณจะได้รับคำแนะนำเบื้องต้นเกี่ยวกับระบบการพิมพ์ในบทเรียนเล็กๆ ที่เข้าใจง่าย เวอร์ชัน Ebook จะพร้อมใช้งานทันที และหนังสือที่พิมพ์จะเป็นข้อมูลอ้างอิงที่ดีสำหรับไลบรารีการเข้ารหัสของคุณ