TypeScript ขั้นสูงพร้อมตัวอย่างจริง (ตอนที่ 1)

เผยแพร่แล้ว: 2020-11-06

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

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

ใช้ซอร์สโค้ดของ Nelio Content เป็นพื้นฐาน

อย่างที่คุณอาจทราบแล้ว Nelio Content เป็นปลั๊กอินที่ให้คุณแชร์เนื้อหาเว็บไซต์ของคุณบนโซเชียลมีเดีย นอกจากนี้ ยังมีฟังก์ชันการทำงานหลายอย่างที่มุ่งช่วยให้คุณสร้างเนื้อหาที่ดีขึ้นอย่างต่อเนื่องในบล็อกของคุณ เช่น การวิเคราะห์คุณภาพของโพสต์ ปฏิทินบรรณาธิการเพื่อติดตามเนื้อหาที่จะมาถึงที่คุณต้องเขียน เป็นต้น .

ปฏิทินบรรณาธิการของ Nelio Content
ปฏิทินบรรณาธิการของ Nelio Content

เมื่อเดือนที่แล้วเราได้เผยแพร่เวอร์ชัน 2.0 ซึ่งเป็นการออกแบบใหม่ทั้งหมดทั้งรูปลักษณ์และภายในปลั๊กอินของเรา เราสร้างเวอร์ชันนี้โดยใช้เทคโนโลยีใหม่ทั้งหมดที่เรามีใน WordPress ในปัจจุบัน (สิ่งที่เราเพิ่งพูดถึงในบล็อกของเราเมื่อเร็วๆ นี้) รวมถึงอินเทอร์เฟซ React และร้าน Redux

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

ตัวเลือกปฏิทินบรรณาธิการเนื้อหา Nelio

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

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

 function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; }

หากคุณสงสัยว่าฉันรู้ได้อย่างไรว่ารัฐมีคุณสมบัติ posts และ days มันค่อนข้างง่าย: เพราะฉันเป็นคนกำหนดมันเอง แต่นี่คือเหตุผลที่ฉันตัดสินใจใช้วิธีนี้

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

  • ในอีกด้านหนึ่ง เรามีแอตทริบิวต์การ posts ต์ซึ่งเราได้แสดงรายการโพสต์ทั้งหมดที่เราได้รับจากเซิร์ฟเวอร์และบันทึกไว้ในร้านค้า Redux ของเรา ตามหลักเหตุผล เราสามารถบันทึกไว้ในอาร์เรย์และทำการค้นหาตามลำดับเพื่อค้นหาโพสต์ที่มี ID ตรงกับที่คาดไว้... แต่วัตถุมีลักษณะเหมือนพจนานุกรมที่ให้การค้นหาที่เร็วขึ้น
  • ในทางกลับกัน เราจำเป็นต้องเข้าถึงโพสต์ที่มีกำหนดการในวันใดวันหนึ่งด้วย อีกครั้ง เราอาจใช้อาร์เรย์เดียวในการจัดเก็บโพสต์ทั้งหมดและกรองเพื่อค้นหาโพสต์ที่เป็นของวันใดวันหนึ่ง แต่การมีพจนานุกรมอื่นช่วยให้ค้นหาได้เร็วขึ้น

การดำเนินการและตัวลดเนื้อหาใน Nelio

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

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

ดังนั้น เมื่อคำนึงถึงสิ่งนี้ นี่คือการกระทำที่เราอาจมีในร้านของเรา:

 function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }

และนี่คือตัวลด:

 function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }

ใช้เวลาในการทำความเข้าใจทั้งหมดแล้วก้าวไปข้างหน้า!

จาก JavaScript ถึง TypeScript

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

สิ่งที่เราต้องทำคือเพิ่มประเภท any ให้กับสิ่งที่ "ซับซ้อน" (เช่นสถานะของแอปพลิเคชันของเรา) อย่างชัดเจน และใช้ number หรือ string หรืออะไรก็ตามที่เราต้องการให้ตัวแปร/อาร์กิวเมนต์อื่นๆ ตัวอย่างเช่น ตัวเลือก JavaScript ดั้งเดิม:

 function getPost( state, id ) { return state.posts[ id ]; }

ด้วยประเภท TypeScript ที่ชัดเจนจะมีลักษณะดังนี้:

 function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; }

อย่างที่คุณเห็น การดำเนินการง่ายๆ ในการพิมพ์โค้ดของเรา (แม้ว่าเราจะใช้ "ประเภททั่วไป") จะให้ข้อมูลจำนวนมากได้อย่างรวดเร็ว การปรับปรุงที่ชัดเจนเมื่อเทียบกับ JavaScript พื้นฐาน! ตัวอย่างเช่น ในกรณีนี้ เราจะเห็นว่า getPost number (ID ของโพสต์เป็นจำนวนเต็ม จำได้ไหม) และผลลัพธ์อาจเป็นบางอย่างหากมีการโพสต์ ( any ) หรือไม่มีเลยหากไม่มี ( undefined )

ที่นี่คุณมีลิงค์พร้อมรหัสทุกประเภทโดยใช้ประเภทง่าย ๆ เพื่อไม่ให้คอมไพเลอร์บ่น

สร้างและใช้ประเภทข้อมูลที่กำหนดเองใน TypeScript

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

การสร้างประเภทที่กำหนดเองสำหรับโพสต์

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

 type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; };

ไม่แปลกใจเลยใช่ไหม Post คือออบเจ็กต์ที่มีแอตทริบิวต์บางอย่าง เช่น id ตัวเลข title ข้อความ และอื่นๆ

ข้อมูลสำคัญอีกชิ้นหนึ่งที่ Redux store มีคือคุณเดาได้ว่าสถานะของมัน ในส่วนที่แล้ว เราได้กล่าวถึงคุณลักษณะที่มีอยู่แล้ว ดังนั้น ให้กำหนดรูปร่างพื้นฐานของประเภท State ของเรา:

 type State = { posts: any; days: any; };

การปรับปรุงประเภท State

ตอนนี้เรารู้แล้วว่า State มีคุณสมบัติสองอย่าง ( posts และ days ) แต่เราไม่รู้อะไรมากเกี่ยวกับสิ่งที่เป็น เพราะมันสามารถเป็น any เราบอกว่าเราต้องการให้คุณลักษณะทั้งสองเป็นพจนานุกรม กล่าวคือ เมื่อได้รับข้อความค้นหา (ไม่ว่าจะเป็น ID ของโพสต์สำหรับ posts หรือวันที่สำหรับ days ) เราต้องการข้อมูลที่เกี่ยวข้อง (โพสต์หรือรายการโพสต์ตามลำดับ) เรารู้ว่าเราสามารถใช้พจนานุกรมโดยใช้วัตถุ แต่เราจะแสดงพจนานุกรมใน TypeScript ได้อย่างไร

หากเราดูที่เอกสารประกอบของ TypeScript เราจะพบว่ามียูทิลิตี้หลายประเภทเพื่อจัดการกับสถานการณ์ทั่วไป โดยเฉพาะอย่างยิ่ง มีประเภทที่เรียกว่า Record ซึ่งดูเหมือนว่าจะเป็นสิ่งที่เราต้องการ: อนุญาตให้เราพิมพ์ตัวแปรโดยใช้คู่ของคีย์/ค่า ซึ่งคีย์นั้นมีประเภท Keys ที่แน่นอน และค่านั้นเป็นประเภท Type หากเราใช้ประเภทนี้กับตัวอย่างของเรา เราจะได้สิ่งนี้:

 type State = { posts: Record<number, Post>; days: Record<string, number[]>; };

จากมุมมองของคอมไพเลอร์ ประเภท Record จะทำงานในลักษณะที่ให้ค่าใด ๆ ของ Keys (ในตัวอย่างของเรา number สำหรับ posts และ string สำหรับ days ) ผลลัพธ์จะเป็นวัตถุประเภท Type เสมอ (ในกรณีของเรา a Post หรือ number[] ตามลำดับ) ปัญหาคือนั่นไม่ใช่วิธีที่เราต้องการให้พจนานุกรมของเราทำงาน: เมื่อเราค้นหาโพสต์เฉพาะโดยใช้ ID เราต้องการให้ผู้เรียบเรียงรู้ว่าเราอาจหรืออาจไม่พบโพสต์ที่เกี่ยวข้อง ซึ่งหมายความว่าผลลัพธ์อาจเป็น Post หรือ undefined

โชคดีที่เราแก้ไขปัญหานี้ได้ง่ายๆ โดยใช้ยูทิลิตี้ประเภทอื่น ได้แก่ ประเภท Partial :

 type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };

ปรับปรุงโค้ดของเราด้วย Type Aliases

ดูแอตทริบิวต์ posts ต์ในรัฐของเรา… คุณเห็นอะไร พจนานุกรมที่จัดทำดัชนี Post ประเภทโพสต์ที่มีตัวเลขใช่ไหม? ตอนนี้ลองนึกภาพตัวเองกำลังตรวจสอบโค้ดนี้ในที่ทำงาน หากคุณพบประเภทดังกล่าว คุณอาจถือว่าโพสต์การจัดทำดัชนี number น่าจะเป็น ID ของโพสต์ที่จัดทำดัชนี... แต่นั่นเป็นเพียงการสันนิษฐาน คุณต้องตรวจสอบรหัสเพื่อให้แน่ใจ แล้ว days ล่ะ ? “สตริงสุ่มสร้างดัชนีรายการตัวเลข” มันไม่ช่วยอะไรมากใช่ไหม?

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

ตัวอย่างเช่น เมื่อรู้ว่ารหัสโพสต์ ( number ) และวันที่ ( string ) เกี่ยวข้องกับโดเมนของเรา เราสามารถสร้างนามแฝงประเภทต่อไปนี้ได้อย่างง่ายดาย:

 type PostId = number; type Day = string;

แล้วเขียนประเภทเดิมของเราใหม่โดยใช้นามแฝงเหล่านี้:

 type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; };

นามแฝงประเภทอื่นที่เราสามารถใช้เพื่อปรับปรุงความสามารถในการอ่านโค้ดของเราคือประเภท Dictionary ซึ่ง "ซ่อน" ความซับซ้อนของการใช้ Partial และ Record อยู่เบื้องหลังโครงสร้างที่สะดวก:

 type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;

ทำให้ซอร์สโค้ดของเราชัดเจนยิ่งขึ้น:

 type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; };

และนั่นแหล่ะ! ที่นั่นคุณมีมัน! ด้วยนามแฝงประเภทง่าย ๆ เพียงสามชื่อ เราจึงสามารถบันทึกโค้ดในลักษณะที่ดีกว่าการใช้ความคิดเห็นได้อย่างชัดเจน นักพัฒนาซอฟต์แวร์รายใดก็ตามที่ตามหลังเราจะสามารถรู้ได้ทันทีว่า posts นั้นเป็นพจนานุกรมที่จัดทำดัชนีวัตถุประเภท Post โดยใช้ PostId ของพวกเขา และใน days นั้นคือโครงสร้างข้อมูลที่ ให้ Day ส่งคืนตัวระบุโพสต์รายการ มันเยี่ยมมากถ้าคุณถามฉัน

แต่ไม่เพียงแต่คำจำกัดความของประเภทเองจะดีกว่า… หากเราใช้ประเภทใหม่เหล่านี้ในโค้ดทั้งหมดของเรา:

 function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }

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

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

ขั้นตอนถัดไป

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

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

ภาพเด่นโดย Danielle MacInnes บน Unsplash