มีอะไรใหม่บ้างใน TypeScript 4.2
TypeScript 4.2 มาพร้อมกับการเพิ่มความสามารถใหม่ flag ใหม่สำหรับคอมไพเลอร์เพื่อระบุใน tsconfig.json และการเปลี่ยนแปลงพฤติกรรมบางอย่างที่อาจเปลี่ยนรูปแบบการใช้งานไปจากเดิม
ความสามารถในการระบุ Rest Elements ตำแหน่งหน้าและกลางของชนิดข้อมูล Tuples
สำหรับชนิดข้อมูลแบบ Tuple ในภาษา TypeScript จะหมายถึงข้อมูลที่มีโครงสร้างแบบอาร์เรย์
แต่ระบุจำนวนข้อมูลที่แน่นอนพร้อมกำหนดชนิดข้อมูลของแต่ละช่องไว้อย่างเรียบร้อย เช่น
สร้างตัวแปร somchai
แบบ Tuple ที่เก็บข้อมูลสองค่าในโครงสร้างแบบอาร์เรย์ โดยช่องแรกคือชื่อและช่องที่สองคืออายุ
1const somchai: [name: string, age: number] = ["Somchai", 24]
สมมติให้มีชนิดข้อมูล Student ที่สามารถระบุ ID, name, advisorId และ Courses โดย Courses หมายถึงคอร์สที่นักเรียนลงทะเบียนจำนวนสองคอร์ส เราสามารถสร้างชนิดข้อมูลดังกล่าวพร้อมกำหนดตัวแปร somchai เพื่อแสดงการใช้งานชนิดข้อมูลนั้นได้ดังนี้
1type Student = [id: number, name: string, advisorId: number, course1: Course, course2: Course];23const somchai: Student = [4 1,5 'Somchai',6 1,7 { id: 1, title: 'Java' },8 { id: 2, title: 'C#' },9];
เป็นที่ทราบกันดีว่านักเรียนไม่จำเป็นต้องเรียนแค่ 2 ชุดวิชา อาจเป็นกี่ชุดวิชาก็ได้ ฉะนั้นแล้วการกำหนดให้ student มีได้แค่สองวิชาตามตัวอย่างดังกล่าวจึงยืดหยุ่นไม่มากพอ
TypeScript 4.0 ได้เพิ่มไวยากรณ์ใหม่ด้วยการอนุญาตให้ใส่เครื่องหมาย Rest (ผ่านสัญลักษณ์ ...
)
เพื่อเป็นตัวบ่งชี้ว่าส่วนนี้สามารถมีจำนวนเท่าใดก็ได้ จากตัวอย่างดังกล่าวเราจึงสามารถแก้ไขให้ชนิดข้อมูล Student
สามารถมีกี่ชุดวิชาก็ได้ ดังนี้
1interface Course {2 id: number;3 title: string;4}56type Student = [id: number, name: string, advisorId: number, ...courses: Course[]];78const somchai: Student = [9 1,10 'Somchai',11 1,12 { id: 1, title: 'Java' },13 { id: 2, title: 'C#' },14];
กำหนดโจทย์ใหม่ให้มีชนิดข้อมูล Classroom สำหรับสร้างห้องเรียนโดยต้องทำการระบุ course ID จากนั้นจึงรับนักเรียนจำนวนเท่าใดก็ได้ และปิดท้ายด้วย ID ของผู้สอนหนึ่งคน ผลลัพธ์ในอุดมคติควรเป็นชนิดข้อมูลที่นิยามได้ดังนี้
1type Classroom = [courseId: number, ...students: Student[], advisorId: number];23// Error: A rest element must be last in a tuple type.4const java: Classroom = [1, somchai, 1];
จากตัวอย่างข้างต้นพบว่าเกิดข้อผิดพลาดคือ A rest element must be last in a tuple type.
นั่นคือ TypeScript อนุญาตให้เรา
ระบุ Rest Elements ได้ในตำแหน่งสุดท้ายของ Tuple เท่านั้น
ปัญหานี้จะหมดไปด้วย TypeScript 4.2 ที่สนับสนุนการใส่เครื่องหมาย Rest ทั้งหน้าสุด ตรงกลาง หรือจะใส่ท้ายสุดของชนิดข้อมูล Tuple ก็ย่อมได้ เพียงแต่มีเงื่อนไขพิเศษอยู่สองข้อคือ
- ในชนิดข้อมูล Tuple นั้นจะมี Rest ได้แค่จุดเดียวเท่านั้น
- ตำแหน่งหลังจากจุดที่ระบุ Rest เป็นต้นไปต้องไม่เป็น Optional Elements
ตัวอย่างต่อไปนี้จะทำให้เกิดข้อผิดพลาด
1// Error: A rest element cannot follow another rest element.2// มี Rest ได้แค่จุดเดียว3type Classroom = [courseId: number, ...students: Student[], ...advisorIds: number[]];45// Error: An optional element cannot follow a rest element.6// ห้ามมี Optional (Element ที่มีเครื่องหมาย ?) ต่อท้าย Rest7type Classroom = [courseId: number, ...students: Student[], advisorId?: number];
การระบุ _ เพื่อบอกว่าเป็นตัวแปรที่ไม่ใช้งานในขั้นตอนของ Destructuring
กำหนดให้มีตัวแปร grades ที่ระบุค่าเกรดของวิชาต่าง ๆ แต่เราต้องการดึงเฉพาะเกรดของ java และ typescript มาใช้งาน
1const [java, python, go, typescript] = arr;23console.log(java, typescript);
กรณีที่เราระบุ --noUnusedLocals จังหวะของการคอมไพล์ TypeScript การกร่นด่าด้วยความสะใจจะเกิดขึ้นว่า
python' is declared but its value is never read.
และ go' is declared but its value is never read.
หรือแปลเป็นภาษาชาวโลกได้ว่า ไม่ใช้งานแล้วจะประกาศตัวแปรมาเพื่อ?
สำหรับ TypeScript 4.2 นั้นเพียงแค่เราระบุ _
หน้าตัวแปรที่ไม่ใช้งานเมื่อทำการ Destructuring
TypeScript ก็จะสงบเงียบไร้การโต้แย้งแม้จะระบุ --noUnusedLocals ก็ตาม
--noPropertyAccessFromIndexSignature
สมมติเรามีชนิดข้อมูลชื่อ Configuration ที่มีโครงสร้างดังต่อไปนี้
1type Configuration = {2 configurable: boolean;3 [key: string]: boolean;4};56const mySettings = {} as Configuration;
จะสังเกตเห็นว่า configurable
เป็น property ที่ถูกกำหนดไว้แล้วตายตัว แต่เพราะเรามีส่วนของ Index Signature ในบรรทัดที่ 6
นั่นทำให้แม้เราพิมพ์ configurable
ผิดเป็น configurrable
TypeScript ก็ยังอนุญาตให้ระบุได้อยู่
1type Configuration = {2 configurable: boolean;3 [key: string]: boolean;4};56const mySettings = {} as Configuration;7mySettings.writable;8mySettings.configurrable = true;
เพื่อป้องกันปัญหานี้ TypeScript 4.2 จึงได้นำเสนอ flag ใหม่ชื่อ --noPropertyAccessFromIndexSignature
โดยเมื่อระบุค่านี้ TypeScript จะไม่อนุญาตให้เข้าถึงค่าของ property ตามกฎของ Index Signature ด้วยการใช้จุด
เช่น mySettings.writable
แต่ต้องระบุผ่านเครื่องหมาย []
แทนเท่านั้น
1// Error: Property 'writable' comes from an index signature,2// so it must be accessed with ['writable'].3mySettings.writable;45// ไม่ error6mySettings['writable'];
ส่วนของ property อื่นที่ระบุตายตัวในชนิดข้อมูลนั้นไว้แล้วให้ใช้เครื่องหมายจุดได้ปกติ
1mySettings.configurable = true;
นั่นหมายความว่าต่อแต่นี้เราจะไม่สามารถพิมพ์คำว่า configurable
ผิดอีกต่อไปเพราะ TypeScript
จะกร่นด่าด้วยความโกรธเกรี้ยวว่าไม่มีคำที่เราพิมพ์ผิดนั้นทันที
การตรวจสอบชนิดข้อมูลจากการใช้ Optional Properties ควบคู่กับ String Index Signatures
จะเกิดอะไรขึ้นเมื่อเรามีการประกาศ String Index Signatures และตั้งใจให้ชนิดข้อมูลอื่นที่มี properties แบบ Optional ถูกโยนค่าใส่ Index Signatures นั้น
1type Courses = {2 [title: string]: number;3};45type Enrollment2021 = {6 Java?: number;7 Python?: number;8 TypeScript?: number;9};1011declare function enroll(courses: Courses): void;12declare const mySelectedCourses: Enrollment2021;
เรามีชนิดข้อมูล Courses แบบ String Index Signatures ที่เป็นตัวแทนของคอร์สทั้งหมด และอีกชนิดข้อมูลคือ Enrollment2021 ที่ใช้สื่อถึงคอร์สที่ลงทะเบียนได้ในปี 2021 โดยแต่ละคอร์สเป็น Optional เพื่อเปิดโอกาส ให้นักเรียนเลือกที่จะเรียนหรือไม่ก็ได้
กำหนดตัวแปร mySelectedCourses
เพื่อเป็นตัวแทนของคอร์สที่เลือกลงทะเบียนในปี 2021 เราต้องการส่งค่านี้ลงไปในฟังก์ชัน enroll
เพื่อทำการลงทะเบียน
1enroll(mySelectedCourses);
และนั่นจึงเป็นต้นตอของ error หลายบรรทัดที่ว่า
1Argument of type 'Enrollment2021' is not assignable to parameter of type 'Courses'.2 Property 'Java' is incompatible with index signature.3 Type 'number | undefined' is not assignable to type 'number'.4 Type 'undefined' is not assignable to type 'number'
สาเหตุของเหตุการณ์นี้เป็นเพราะ Courses ไม่อนุญาตให้แต่ละ properties เป็น Optional ได้นั่นเอง แต่เมื่อทำการอัพเกรตสู่ TypeScript 4.2 ข้อผิดพลาดดังกล่าวก็จะหายไป
TypeScript 4.2 อนุญาตให้ใช้ Optional Properties กับ String Index Signatures ได้ แต่ไม่อนุญาตให้ Properties ปกติ ที่ไม่ใช่ Optional แต่มีโอกาสเป็น undefined ใช้งานได้กับ String Index Signatures เช่นตัวอย่างต่อไปนี้
1// error!2type Enrollment2021 = {3 Java: number | undefined; // ไม่ใช่ Optional และมีโอกาสเป็น undefined4 Python: number | undefined;5 TypeScript: number | undefined;6};
นอกจากนี้กฎดังกล่าวจะใช้ไม่ได้กับกรณีของ Number Index Signatures เนื่องจากชนิดข้อมูลนั้นถูกใช้ในฐานะโครงสร้างเสมือนอาร์เรย์
1type Courses = {2 [id: number]: string;3};45type Enrollment2021 = {6 1?: string;7 2?: string;8};910declare let myCourses: Enrollment2021;11declare let courses: Courses;1213// error14courses = myCourses;
Abstract Construct Signatures
กำหนดให้มีคลาส Account แทนบัญชีเงินฝากซึ่งสามารถมีคลาสลูกเป็น SavingAccount หรือบัญชีเงินฝากประจำได้
โดยทุกครั้งที่ทำธุรกรรมให้มีการบันทึกธุรกรรมเหล่านั้นลงตัวแปร transactions
1abstract class Account {2 protected transactions: string[] = [];34 constructor(protected balance: number) {}56 protected record(action: string, amount: number) {7 this.transactions.push(`${action}: ${amount}`);8 }910 public abstract deposit(amount: number): void;11 public abstract withdraw(amount: number): void;12}1314class SavingAccount extends Account {15 public deposit(amount: number): void {16 this.balance += amount;17 this.record('deposit', amount);18 }1920 public withdraw(amount: number): void {21 if (this.balance < amount) return;2223 this.balance -= amount;24 this.record('withdraw', amount);25 }26}
เราอยากที่จะสร้างฟังก์ชัน iter
ที่มีความสามารถแปลง SavingAccount ของเราให้เป็น Iterable Object หรือออบเจ็กต์ที่วนลูปได้
เราจึงทำการสร้าง iter
ดังนี้
1type Constructor<T> = new (...args: any[]) => T;23function iter<Klass extends Constructor<object>>(Ctor: Klass, prop: string) {4 abstract class Iter extends Ctor {5 [Symbol.iterator]() {6 return (this as any)[prop].values();7 }8 }910 return Iter;11}
และทำการเรียกใช้งาน iter ควบคู่กับ SavingAccount ในลักษณะของ Mixin
1class SavingAccount extends iter(Account, 'transactions') {2 public deposit(amount: number): void {3 this.balance += amount;4 this.record('deposit', amount);5 }67 public withdraw(amount: number): void {8 if (this.balance < amount) return;910 this.balance -= amount;11 this.record('withdraw', amount);12 }13}
เราคาดหวังว่าทุกการทำธุรกรรมของเราควรจะได้รับผลลัพธ์อย่างถูกต้อง
1const myAcc = new SavingAccount(500);2myAcc.deposit(1000);3myAcc.deposit(500);4myAcc.withdraw(300);56// [LOG]: "deposit: 1000"7// [LOG]: "deposit: 500"8// [LOG]: "withdraw: 300"9for (const transaction of myAcc) {10 console.log(transaction);11}
ไม่เป็นเช่นนั้นเพราะ TypeScript จะพ่นข้อผิดพลาดออกมาในช่วงการสร้าง SavingAccount
1// Argument of type 'typeof Account' is not assignable to parameter of type 'Constructor<object>'.2// Cannot assign an abstract constructor type to a non-abstract constructor type.3class SavingAccount extends iter(Account, 'transactions') {}
เนื่องจาก Account เป็น abstract class จึงไม่สามารถกำหนดชนิดข้อมูลนี้ให้กับ Constructor<T>
ที่รับชนิดข้อมูลแบบ concreate class ได้
สำหรับ TypeScript 4.2 ระบุ abstract นำหน้าตัวสร้าง (constructor signatures) ได้ เมื่อเราแก้ไขส่วนของ Constructor<T>
โค้ดทั้งหมดก็จะกลับมาทำงานได้อย่างที่ควรเป็น
1type Constructor<T> = abstract new (...args: any[]) => T;
explainFiles
เพื่อให้โปรแกรมเมอร์มีความเข้าใจมากขึ้นว่าทำไม TypeScript ถึง includes ไฟล์เหล่านี้ในจังหวะของการทำงาน TypeScript 4.2 จึงได้เตรียมคำสั่งสำหรับ Log ข้อมูลของการเรียกใช้ไฟล์เหล่านั้นพร้อมอธิบายว่าไฟล์ที่ถูกเรียกนั้นเป็นเพราะมีไฟล์ใดต้องการเข้าถึง
1tsc --explainFiles
Type Alias Preservation
สมมติเรามีฟังก์ชัน shiftRight
ที่รับ position เป็นชนิดข้อมูล Unit
และคืนกลับเป็น Unit | undefined
1type Unit = number | string;23function shiftRight(position: Unit) {4 if (Math.random() < 0.5) {5 return undefined;6 }78 return position;9}
ในเวอร์ชันก่อนหน้าของ TypeScript หากพิจารณาดูชนิดข้อมูลของฟังก์ชัน shiftRight
จะพบว่าฟังก์ชันดังกล่าวมีชนิดข้อมูลเป็น
1function shiftRight(position: Unit): string | number | undefined;
นั่นเป็นเพราะเมื่อทำการสร้างชนิดข้อมูลแบบ Union ต่อจากตัวเดิม (ในกรณีนี้คือเริ่มต้นที่ position เป็น Union แต่ข้อมูลคืนกลับจากฟังก์ชันเป็น Unit | undefined)
TypeScript จะทำการย่อยชนิดข้อมูล Union นั้นลงทำให้เหลือเป็นข้อมูลพื้นฐานจาก Unit | undefined
เป็น string | number | undefined
แทน
สำหรับ TypeScript 4.2 ไม่เป็นเช่นนั้น คอมไพเลอร์มีความฉลาดมากขึ้นผลลัพธ์จึงออกมาเป็น
1function shiftRight(position: Unit): Unit | undefined;
อื่น ๆ
นอกเหนือจากความสามารถที่ได้กล่าวมานี้ TypeScript ยังมีความสามารถเพิ่มเติมอื่นอีกรวมถึงคุณสมบัติบางอย่างที่สามารถใช้งานได้ในเวอร์ชันก่อน แต่มีพฤติกรรมเปลี่ยนไปในเวอร์ชันนี้ ผู้อ่านสามารถดูข้อมูลเพิ่มเติมได้จาก Announcing TypeScript 4.2
เรียนรู้ TypeScript อย่างมืออาชีพ
คอร์สออนไลน์ Comprehensive TypeScript คอร์สสอนการใช้งาน TypeScript ตั้งแต่เริ่มต้นจนถึงขั้นสูง เรียนรู้หลักการทำงานของ TypeScript การประกาศชนิดข้อมูลต่าง ๆ พร้อมการใช้งานขั้นสูงพร้อมรองรับการทำงานกับ TypeScript เวอร์ชัน 4.2 ด้วย
เอกสารอ้างอิง
Daniel (2021). Announcing TypeScript 4.2. Retrieved Febuary, 28, 2021, from https://devblogs.microsoft.com/typescript/announcing-typescript-4-2
สารบัญ
- ความสามารถในการระบุ Rest Elements ตำแหน่งหน้าและกลางของชนิดข้อมูล Tuples
- การระบุ _ เพื่อบอกว่าเป็นตัวแปรที่ไม่ใช้งานในขั้นตอนของ Destructuring
- --noPropertyAccessFromIndexSignature
- การตรวจสอบชนิดข้อมูลจากการใช้ Optional Properties ควบคู่กับ String Index Signatures
- Abstract Construct Signatures
- explainFiles
- Type Alias Preservation
- อื่น ๆ
- เรียนรู้ TypeScript อย่างมืออาชีพ
- เอกสารอ้างอิง