มีอะไรใหม่บ้างใน TypeScript 4.2

Nuttavut Thongjor

TypeScript 4.2 มาพร้อมกับการเพิ่มความสามารถใหม่ flag ใหม่สำหรับคอมไพเลอร์เพื่อระบุใน tsconfig.json และการเปลี่ยนแปลงพฤติกรรมบางอย่างที่อาจเปลี่ยนรูปแบบการใช้งานไปจากเดิม

ความสามารถในการระบุ Rest Elements ตำแหน่งหน้าและกลางของชนิดข้อมูล Tuples

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

TypeScript
1const somchai: [name: string, age: number] = ["Somchai", 24]

สมมติให้มีชนิดข้อมูล Student ที่สามารถระบุ ID, name, advisorId และ Courses โดย Courses หมายถึงคอร์สที่นักเรียนลงทะเบียนจำนวนสองคอร์ส เราสามารถสร้างชนิดข้อมูลดังกล่าวพร้อมกำหนดตัวแปร somchai เพื่อแสดงการใช้งานชนิดข้อมูลนั้นได้ดังนี้

TypeScript
1type Student = [id: number, name: string, advisorId: number, course1: Course, course2: Course];
2
3const 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 สามารถมีกี่ชุดวิชาก็ได้ ดังนี้

TypeScript
1interface Course {
2 id: number;
3 title: string;
4}
5
6type Student = [id: number, name: string, advisorId: number, ...courses: Course[]];
7
8const somchai: Student = [
9 1,
10 'Somchai',
11 1,
12 { id: 1, title: 'Java' },
13 { id: 2, title: 'C#' },
14];

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

TypeScript
1type Classroom = [courseId: number, ...students: Student[], advisorId: number];
2
3// 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

ตัวอย่างต่อไปนี้จะทำให้เกิดข้อผิดพลาด

TypeScript
1// Error: A rest element cannot follow another rest element.
2// มี Rest ได้แค่จุดเดียว
3type Classroom = [courseId: number, ...students: Student[], ...advisorIds: number[]];
4
5// Error: An optional element cannot follow a rest element.
6// ห้ามมี Optional (Element ที่มีเครื่องหมาย ?) ต่อท้าย Rest
7type Classroom = [courseId: number, ...students: Student[], advisorId?: number];

การระบุ _ เพื่อบอกว่าเป็นตัวแปรที่ไม่ใช้งานในขั้นตอนของ Destructuring

กำหนดให้มีตัวแปร grades ที่ระบุค่าเกรดของวิชาต่าง ๆ แต่เราต้องการดึงเฉพาะเกรดของ java และ typescript มาใช้งาน

TypeScript
1const [java, python, go, typescript] = arr
2
3console.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 ที่มีโครงสร้างดังต่อไปนี้

TypeScript
1type Configuration = {
2 configurable: boolean
3 [key: string]: boolean
4}
5
6const mySettings = {} as Configuration

จะสังเกตเห็นว่า configurable เป็น property ที่ถูกกำหนดไว้แล้วตายตัว แต่เพราะเรามีส่วนของ Index Signature ในบรรทัดที่ 6 นั่นทำให้แม้เราพิมพ์ configurable ผิดเป็น configurrable TypeScript ก็ยังอนุญาตให้ระบุได้อยู่

TypeScript
1type Configuration = {
2 configurable: boolean
3 [key: string]: boolean
4}
5
6const mySettings = {} as Configuration
7mySettings.writable
8mySettings.configurrable = true

เพื่อป้องกันปัญหานี้ TypeScript 4.2 จึงได้นำเสนอ flag ใหม่ชื่อ --noPropertyAccessFromIndexSignature โดยเมื่อระบุค่านี้ TypeScript จะไม่อนุญาตให้เข้าถึงค่าของ property ตามกฎของ Index Signature ด้วยการใช้จุด เช่น mySettings.writable แต่ต้องระบุผ่านเครื่องหมาย [] แทนเท่านั้น

TypeScript
1// Error: Property 'writable' comes from an index signature,
2// so it must be accessed with ['writable'].
3mySettings.writable
4
5// ไม่ error
6mySettings['writable']

ส่วนของ property อื่นที่ระบุตายตัวในชนิดข้อมูลนั้นไว้แล้วให้ใช้เครื่องหมายจุดได้ปกติ

TypeScript
1mySettings.configurable = true

นั่นหมายความว่าต่อแต่นี้เราจะไม่สามารถพิมพ์คำว่า configurable ผิดอีกต่อไปเพราะ TypeScript จะกร่นด่าด้วยความโกรธเกรี้ยวว่าไม่มีคำที่เราพิมพ์ผิดนั้นทันที

การตรวจสอบชนิดข้อมูลจากการใช้ Optional Properties ควบคู่กับ String Index Signatures

จะเกิดอะไรขึ้นเมื่อเรามีการประกาศ String Index Signatures และตั้งใจให้ชนิดข้อมูลอื่นที่มี properties แบบ Optional ถูกโยนค่าใส่ Index Signatures นั้น

TypeScript
1type Courses = {
2 [title: string]: number
3}
4
5type Enrollment2021 = {
6 Java?: number
7 Python?: number
8 TypeScript?: number
9}
10
11declare function enroll(courses: Courses): void
12declare const mySelectedCourses: Enrollment2021

เรามีชนิดข้อมูล Courses แบบ String Index Signatures ที่เป็นตัวแทนของคอร์สทั้งหมด และอีกชนิดข้อมูลคือ Enrollment2021 ที่ใช้สื่อถึงคอร์สที่ลงทะเบียนได้ในปี 2021 โดยแต่ละคอร์สเป็น Optional เพื่อเปิดโอกาส ให้นักเรียนเลือกที่จะเรียนหรือไม่ก็ได้

กำหนดตัวแปร mySelectedCourses เพื่อเป็นตัวแทนของคอร์สที่เลือกลงทะเบียนในปี 2021 เราต้องการส่งค่านี้ลงไปในฟังก์ชัน enroll เพื่อทำการลงทะเบียน

TypeScript
1enroll(mySelectedCourses)

และนั่นจึงเป็นต้นตอของ error หลายบรรทัดที่ว่า

Code
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 เช่นตัวอย่างต่อไปนี้

TypeScript
1// error!
2type Enrollment2021 = {
3 Java: number | undefined // ไม่ใช่ Optional และมีโอกาสเป็น undefined
4 Python: number | undefined
5 TypeScript: number | undefined
6}

นอกจากนี้กฎดังกล่าวจะใช้ไม่ได้กับกรณีของ Number Index Signatures เนื่องจากชนิดข้อมูลนั้นถูกใช้ในฐานะโครงสร้างเสมือนอาร์เรย์

TypeScript
1type Courses = {
2 [id: number]: string
3}
4
5type Enrollment2021 = {
6 1?: string
7 2?: string
8}
9
10declare let myCourses: Enrollment2021
11declare let courses: Courses
12
13// error
14courses = myCourses

Abstract Construct Signatures

กำหนดให้มีคลาส Account แทนบัญชีเงินฝากซึ่งสามารถมีคลาสลูกเป็น SavingAccount หรือบัญชีเงินฝากประจำได้ โดยทุกครั้งที่ทำธุรกรรมให้มีการบันทึกธุรกรรมเหล่านั้นลงตัวแปร transactions

TypeScript
1abstract class Account {
2 protected transactions: string[] = []
3
4 constructor(protected balance: number) {}
5
6 protected record(action: string, amount: number) {
7 this.transactions.push(`${action}: ${amount}`)
8 }
9
10 public abstract deposit(amount: number): void
11 public abstract withdraw(amount: number): void
12}
13
14class SavingAccount extends Account {
15 public deposit(amount: number): void {
16 this.balance += amount
17 this.record('deposit', amount)
18 }
19
20 public withdraw(amount: number): void {
21 if (this.balance < amount) return
22
23 this.balance -= amount
24 this.record('withdraw', amount)
25 }
26}

เราอยากที่จะสร้างฟังก์ชัน iter ที่มีความสามารถแปลง SavingAccount ของเราให้เป็น Iterable Object หรือออบเจ็กต์ที่วนลูปได้ เราจึงทำการสร้าง iter ดังนี้

TypeScript
1type Constructor<T> = new (...args: any[]) => T
2
3function 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 }
9
10 return Iter
11}

และทำการเรียกใช้งาน iter ควบคู่กับ SavingAccount ในลักษณะของ Mixin

TypeScript
1class SavingAccount extends iter(Account, 'transactions') {
2 public deposit(amount: number): void {
3 this.balance += amount
4 this.record('deposit', amount)
5 }
6
7 public withdraw(amount: number): void {
8 if (this.balance < amount) return
9
10 this.balance -= amount
11 this.record('withdraw', amount)
12 }
13}

เราคาดหวังว่าทุกการทำธุรกรรมของเราควรจะได้รับผลลัพธ์อย่างถูกต้อง

TypeScript
1const myAcc = new SavingAccount(500)
2myAcc.deposit(1000)
3myAcc.deposit(500)
4myAcc.withdraw(300)
5
6// [LOG]: "deposit: 1000"
7// [LOG]: "deposit: 500"
8// [LOG]: "withdraw: 300"
9for (const transaction of myAcc) {
10 console.log(transaction)
11}

ไม่เป็นเช่นนั้นเพราะ TypeScript จะพ่นข้อผิดพลาดออกมาในช่วงการสร้าง SavingAccount

TypeScript
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> โค้ดทั้งหมดก็จะกลับมาทำงานได้อย่างที่ควรเป็น

TypeScript
1type Constructor<T> = abstract new (...args: any[]) => T;

explainFiles

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

Code
1tsc --explainFiles

Type Alias Preservation

สมมติเรามีฟังก์ชัน shiftRight ที่รับ position เป็นชนิดข้อมูล Unit และคืนกลับเป็น Unit | undefined

TypeScript
1type Unit = number | string
2
3function shiftRight(position: Unit) {
4 if (Math.random() < 0.5) {
5 return undefined
6 }
7
8 return position
9}

ในเวอร์ชันก่อนหน้าของ TypeScript หากพิจารณาดูชนิดข้อมูลของฟังก์ชัน shiftRight จะพบว่าฟังก์ชันดังกล่าวมีชนิดข้อมูลเป็น

TypeScript
1function shiftRight(position: Unit): string | number | undefined

นั่นเป็นเพราะเมื่อทำการสร้างชนิดข้อมูลแบบ Union ต่อจากตัวเดิม (ในกรณีนี้คือเริ่มต้นที่ position เป็น Union แต่ข้อมูลคืนกลับจากฟังก์ชันเป็น Unit | undefined) TypeScript จะทำการย่อยชนิดข้อมูล Union นั้นลงทำให้เหลือเป็นข้อมูลพื้นฐานจาก Unit | undefined เป็น string | number | undefined แทน

สำหรับ TypeScript 4.2 ไม่เป็นเช่นนั้น คอมไพเลอร์มีความฉลาดมากขึ้นผลลัพธ์จึงออกมาเป็น

TypeScript
1function shiftRight(position: Unit): Unit | undefined

อื่น ๆ

นอกเหนือจากความสามารถที่ได้กล่าวมานี้ TypeScript ยังมีความสามารถเพิ่มเติมอื่นอีกรวมถึงคุณสมบัติบางอย่างที่สามารถใช้งานได้ในเวอร์ชันก่อน แต่มีพฤติกรรมเปลี่ยนไปในเวอร์ชันนี้ ผู้อ่านสามารถดูข้อมูลเพิ่มเติมได้จาก Announcing 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
  • อื่น ๆ
  • เอกสารอ้างอิง