มีอะไรใหม่บ้างใน TypeScript 4.4
TypeScript 4.4 ได้ถูกปล่อยออกมาเรียบร้อยแล้ว มาดูกันซิว่ามีฟีเจอร์ใหม่อะไรให้ใช้งานบ้าง
กำหนดพรอพเพอร์ตี้ด้วย Symbol และ Template String ใน Index Signatures
ก่อนหน้านี้การกำหนดชนิดข้อมูลให้กับ Index Signatures ส่วนของพร็อพเพอร์ตี้ต้องมีชนิดข้อมูลเป็น string หรือ number เท่านั้น เช่น
1interface Options {2 // property มี key เป็น string3 [key: string]: unknown4}56const options: Options = { writeable: true }
TypeScript 4.4 อนุญาตให้เราสามารถใช้ Template Literal เพื่อกำหนดรูปแบบข้อความให้ปรากฎเป็นค่า key ของพร็อพเพอร์ตี้ได้
เช่นกรณีที่เราต้องการให้พร็อกเพอร์ตี้เป็นข้อความที่ขึ้นต้นด้วยคำว่า data-
เราสามารถกำหนด key ด้วย data-${string}
ที่มีนัยยะว่าพร็อพเพอร์ตี้นั้นต้องขึ้นต้นด้วยคำว่า data-
และลงท้ายด้วย string ใด ๆ ก็ได้
1interface DataAttrs {2 [key: `data-${string}`]: string;3}45const linkAttrs: DataAttrs = {6 'data-testid': 'my-link',7 'data-anchor': 'my-link',8 'data-color': '#ee66ee'9}
นอกจากนี้ TypeScript 4.4 ยังอนุญาตให้ใช้ชนิดข้อมูล Symbol เป็นพรอพเพอร์ตี้ได้เช่นกัน
1interface SpecialTypes<T> {2 [sym: symbol]: (value: T) => void3}45const types: SpecialTypes<string> = {6 [Symbol('ITERATION')]: (item) => {7 /* ... */8 },9 [Symbol('INSERTION')]: (item) => {10 /* ... */11 },12}
เมื่อ TypeScript 4.4 มาถึง Index Signatures จึงอนุญาตให้เราใช้ชนิดข้อมูลได้ถึงสี่ประเภทคือ string, number, symbol และ template string
โดยแต่ละชนิดข้อมูลยังสามารถเชื่อมต่อกันผ่านชนิดข้อมูล union (|
) ได้อีกด้วย
1interface KeyPair {2 [key: string | symbol]: unknown3}
Exact Optional Property Types
ออบเจ็กต์บน JavaScript หากเข้าถึงพรอพเพอร์ตี้ที่ไม่มีจริงเราย่อมได้ค่า undefined กลับออกมา ผลลัพธ์ดังกล่าวเป็นเช่นเดียวกับกรณีที่เข้าถึงพรอพเพอร์ตี้ที่มีค่าเป็น undefined
1const person = {2 name: 'Somchai',3 age: 24,4 tel: undefined,5}67console.log(person.tel) // undefined8console.log(person.address) // undefined
เมื่อเป็นเช่นนี้ TypeScript จึงถือว่ากรณีของการใช้ Optional Property ก็ควรอนุญาตให้กำหนด undefined ให้กับมันได้ด้วย พิจารณาพรอพเพอร์ตี้ address ที่เป็น Optional Property ต่อไปนี้
1interface Person {2 name: string3 age: number4 tel: string | undefined5 address?: string6}
TypeScript จะถือว่าพรอพเพอร์ตี้ดังกล่าวมีชนิดข้อมูลเดียวกันกับโค้ดข้างล่างที่อนุญาตให้กำหนด undefined ให้กับมันได้โดยตรง
1interface Person {2 name: string3 age: number4 tel: string | undefined5 address?: string | undefined6}
กำหนดให้มีตัวแปร person ที่มีชนิดข้อมูลเป็น Person ดังต่อไปนี้
1const person: Person = {2 name: 'Somchai',3 age: 24,4 tel: undefined,5}
กรณีของโค้ดข้างต้นเรากล่าวได้ว่าทั้ง person.tel
และ person.address
ต่างได้ค่าเป็น undefined
1console.log(person.tel) // undefined2console.log(person.address) // undefined
แม้เราจะระบุ tel
ให้มีค่าเป็น undefined และละเว้นค่า address
เอาไว้ แต่ในโค้ดของเราก็ยังยากที่จะทราบอยู่ดี
ว่า tel
และ address
ได้ค่าเป็น undefined เนื่องจากไม่มีพรอพเพอร์ตี้นี้ในออบเจ็กต์หรือ
เป็นเพราะมีพรอพเพอร์ตี้แต่ค่าของมันเป็น undefined กันแน่
เราจึงต้องพึ่งการดำเนินการบางอย่างเช่นการใช้ in
ในการตรวจสอบแทน
1'name' in person // true2'address' in person // false3'tel' in person // true
เพื่อให้จำแนกได้อย่างชัดเจน TypeScript จึงไม่ควรกำหนดให้ Optional Property สามารถกำหนด undefined ให้กับมันได้โดยตรง
กล่าวคือชนิดข้อมูล Person ข้างต้นนี้เมื่อกำหนดพรอพเพอร์ตี้ address เป็น address?: string
ย่อมไม่ควรให้มีความหมายเดียวกับ
address?: string | undefined
นั่นเอง
TypeScript 4.4 สามารถระบุตัวเลือกขอคอมไพเลอร์ด้วย flag --exactOptionalPropertyTypes
ค่านี้เป็นการแจ้ง TypeScript ให้ทราบว่าอย่าเติม | undefined
ให้กับ Optional Property
1interface Person {2 name: string3 age: number4 tel: string | undefined5 address?: string6}78const person: Person = {9 name: 'Somchai',10 age: 24,11 tel: undefined,12}1314// กรณีไม่ได้เปิดใช้ --exactOptionalPropertyTypes15// ชนิดข้อมูลเป็น string | undefined16person.address1718// สามารถทำได้19person.address = undefined2021// กรณีไม่ได้เปิดใช้ --exactOptionalPropertyTypes22// ชนิดข้อมูลเป็น string เท่านั้น23person.address2425// ไม่สามารถทำได้26person.address = undefined
Static Block ในคลาส
TypeScript 4.4 อนุญาตให้เราสร้าง static block ในคลาสได้ static block เป็นส่วนของบลอคที่จะถูกเรียกครั้งเดียวเมื่อคลาสดังกล่าวถูกโหลดเข้าสู่หน่วยความจำ โดยการทำงานของบลอคนี้จะทำก่อนส่วนของ constructor เสมอ
1class Foo {2 static #preferences = 0;34 get preferences() {5 return Foo.#preferences;6 }78 static {9 try {10 const preferences = loadPreferencesFromFile('./preferences.yml');1112 Foo.#preferences = preferences;13 } catch {14 // handles errors15 }16 }17}
static block ในคลาสไม่จำเป็นต้องมีบลอคเดียว แต่สามารถมีหลายบลอคได้โดยลำดับการทำงานจะเรียงต่อกันจากบนลงล่างตามลำดับโค้ด
การกำหนดชนิดข้อมูล unknown สำหรับตัวแปรใน catch ของประโยค try/catch
ส่วนของบลอค try ในภาษา JavaScript สามารถโยนข้อผิดพลาดด้วยชนิดข้อมูลใด ๆ ก็ได้ เช่น Error หรือ TypeError ด้วยเหตุนี้ตัวแปรสำหรับการรับค่าใน catch TypeScript จึงกำหนดชนิดข้อมูลให้เป็น any
1try {2 throw new Error('Error Na Ja')3 // หรือ4 throw new TypeError('Error Ei Ei')56 // เพื่อให้ ex สามารถรองรับการ throw ที่ต่างชนิดข้อมูลกันได้7 // ex จึงมีชนิดข้อมูลเป็น any8} catch (ex) {}
เป็นที่ทราบกันดีว่าการใช้ any นั้นเอื้อให้เกิดข้อผิดพลาดได้ง่าย เนื่องจาก any จะไม่ถูกตรวจสอบชนิดข้อมูลเมื่อคอมไพล์
1try {2 throw new Error('Error Na Ja')3 // หรือ4 throw new TypeError('Error Ei Ei')5} catch (ex) {6 // เรียกใช้ได้ TypeScript ไม่ตรวจสอบเนื่องจาก ex เป็น any7 console.log(ex.eiei)8 console.log(ex.naja)9}
TypeScript 4.4 มีแฟลคใหม่ชื่อ --useUnknownInCatchVariables
ที่เมื่อเปิดใช้แล้วจะทำให้ชนิดข้อมูลของตัวแปร
ใน catch เป็น unknown กรณีที่เปิดแฟลค --strict
ก็จะรวมการทำงานของ --useUnknownInCatchVariables
เข้าไว้แล้วด้วยเช่นกัน
1// กำหนดให้เปิดใช้ --useUnknownInCatchVariables2// หรือ --strict3try {4 throw new Error('Error Na Ja')5} catch (ex) {6 // Property 'eiei' does not exist on type 'unknown'.7 // ไม่สามารถเรียก eiei ได้8 console.log(ex.eiei)910 // ทำงานได้อย่างถูกต้อง11 if (ex instanceof Error) {12 console.error(ex.message)13 }14}
การปรับปรุงการทำงานของ Control Flow สำหรับประโยคเงื่อนไขและ Discriminated Unions
สำหรับภาษา TypeScript type guard คือกระบวนการตรวจสอบชนิดข้อมูลเพื่อจำกัดขอบเขตของชนิดข้อมูลนั้นให้แคบลง
โดยอาศัยประโยคเงื่อนไข เช่น if
ในการทำงาน
1function squeeze<T extends string | number>(item: T) {2 // type guard ตรวจสอบ word ก่อนว่าเป็น string หรือไม่3 if (typeof item === 'string') {4 // TypeScript จะทราบว่า item เป็น string5 // จึงสามารถใช้เมธอด replace ของ string ได้6 return item.replace(/[_\t\n ]/g, '')7 }89 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)10}
กรณีที่เราแยกเงื่อนไขการตรวจสอบออกนอกประโยคเงื่อนไข if TypeScript จะไม่สามารถจำแนกได้อีกต่อไปว่า item มีชนิดข้อมูล เป็น string ไม่ใช่ T
1function squeeze<T extends string | number>(item: T) {2 const isString = typeof item === 'string'34 if (isString) {5 // Property 'replace' does not exist on type 'T'6 return item.replace(/[_\t\n ]/g, '')7 }89 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)10}
สำหรับ TypeScript 4.4 หากตัวแปรสำหรับการทดสอบนั้นเป็นค่าคงที่ (const), readonly หรือเป็นการประกาศที่ไม่อนุญาตให้ตัวแปรเปลี่ยนแปลงค่าได้ TypeScript จะสามารถอนุมานการทำงานของ type guard นั้นได้อย่างถูกต้อง
1// สำหรับ TypeScript 4.42function squeeze<T extends string | number>(item: T) {3 // เนื่องจากตัวแปรเป็น const4 const isString = typeof item === 'string'56 // type guard ทำงานได้ถูกต้อง7 if (isString) {8 return item.replace(/[_\t\n ]/g, '')9 }1011 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)12}
หากเปลี่ยนตัวแปร isString ของเราเป็น let ที่อนุญาตให้แก้ไขค่าข้อมูลภายหลังได้ TypeScript จะไม่ทราบว่าก่อนหน้า type guard ตัวแปรจะถูกเปลี่ยนค่าเป็นอย่างอื่นอีกหรือไม่ การทำงานจึงไม่สมบูรณ์
1// สำหรับ TypeScript 4.42function squeeze<T extends string | number>(item: T) {3 // เนื่องจากตัวแปรเป็น let อาจถูกแก้ไขค่าภายหลังได้4 let isString = typeof item === 'string'56 // type guard ทำงานได้ไม่ถูกต้อง7 if (isString) {8 // ยังคงมองเห็น item เป็นชนิดข้อมูล T9 // Property 'replace' does not exist on type 'string | number'.10 return item.replace(/[_\t\n ]/g, '')11 }1213 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)14}
ผลลัพธ์จากการปรับปรุงประสิทธิภาพในการวิเคราะห์ในส่วนนี้ยังส่งผลถึงการทำงานของ Discriminated Union ด้วย ก่อนหน้านี้ถ้าเราต้องการตรวจสอบ Tag ใด ๆ เราไม่สามารถ destructuring ตัวแปร Tag นั้นได้ก่อน
1type InputProps =2 | { variant: 'input'; capture?: boolean | string | undefined }3 | { variant: 'textarea'; cols?: number | undefined }45const Input = (props: InputProps) => {6 const { variant } = props78 if (variant === 'input') {9 props.capture // error!1011 // render input12 } else {13 props.cols // error!1415 // render textarea16 }17}
จากตัวอย่างข้างต้น Tag ของเราคือค่า variant ก่อนการมาของ TypeScript 4.4 หากเรา destructure ค่านี้ออกมาก่อนแล้วจึงนำตัวแปรมาเทียบในภายหลัง (บรรทัดที่ 8) ในขอบเขตของ if TypeScript จะไม่สามารถอนุมานได้ว่า props นั้นถูกจำกัดขอบเขตให้เป็น variant แบบ input แล้ว เราจึงไม่สามารถเรียก capture ได้นั่นเอง
ตัวอย่างเดียวกันแต่เปลี่ยน TypeScript เป็นเวอร์ชัน 4.4 จะพบว่าข้อจำกัดนี้ได้หายไปเป็นที่เรียบร้อย
การปรับปรุงประสิทธิภาพและการเปลี่ยนแปลง
TypeScript 4.4 ยังมีการปรับปรุงประสิทธิภาพการทำงานอีกมาก รวมถึง Breaking Changes บางส่วนด้วย สามารถอ่านรายละเอียด เพิ่มเติมได้จาก Announcing TypeScript 4.4
เรียนรู้ TypeScript อย่างมืออาชีพ
คอร์สออนไลน์ Comprehensive TypeScript คอร์สสอนการใช้งาน TypeScript ตั้งแต่เริ่มต้นจนถึงขั้นสูง เรียนรู้หลักการทำงานของ TypeScript การประกาศชนิดข้อมูลต่าง ๆ พร้อมการใช้งานขั้นสูงพร้อมรองรับการทำงานกับ TypeScript เวอร์ชัน 4.4 ด้วย
สารบัญ
- กำหนดพรอพเพอร์ตี้ด้วย Symbol และ Template String ใน Index Signatures
- Exact Optional Property Types
- Static Block ในคลาส
- การกำหนดชนิดข้อมูล unknown สำหรับตัวแปรใน catch ของประโยค try/catch
- การปรับปรุงการทำงานของ Control Flow สำหรับประโยคเงื่อนไขและ Discriminated Unions
- การปรับปรุงประสิทธิภาพและการเปลี่ยนแปลง
- เรียนรู้ TypeScript อย่างมืออาชีพ