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

Nuttavut Thongjor

TypeScript 4.4 ได้ถูกปล่อยออกมาเรียบร้อยแล้ว มาดูกันซิว่ามีฟีเจอร์ใหม่อะไรให้ใช้งานบ้าง

กำหนดพรอพเพอร์ตี้ด้วย Symbol และ Template String ใน Index Signatures

ก่อนหน้านี้การกำหนดชนิดข้อมูลให้กับ Index Signatures ส่วนของพร็อพเพอร์ตี้ต้องมีชนิดข้อมูลเป็น string หรือ number เท่านั้น เช่น

TypeScript
1interface Options {
2 // property มี key เป็น string
3 [key: string]: unknown
4}
5
6const options: Options = { writeable: true }

TypeScript 4.4 อนุญาตให้เราสามารถใช้ Template Literal เพื่อกำหนดรูปแบบข้อความให้ปรากฎเป็นค่า key ของพร็อพเพอร์ตี้ได้ เช่นกรณีที่เราต้องการให้พร็อกเพอร์ตี้เป็นข้อความที่ขึ้นต้นด้วยคำว่า data- เราสามารถกำหนด key ด้วย data-${string} ที่มีนัยยะว่าพร็อพเพอร์ตี้นั้นต้องขึ้นต้นด้วยคำว่า data- และลงท้ายด้วย string ใด ๆ ก็ได้

TypeScript
1interface DataAttrs {
2 [key: `data-${string}`]: string;
3}
4
5const linkAttrs: DataAttrs = {
6 'data-testid': 'my-link',
7 'data-anchor': 'my-link',
8 'data-color': '#ee66ee'
9}

นอกจากนี้ TypeScript 4.4 ยังอนุญาตให้ใช้ชนิดข้อมูล Symbol เป็นพรอพเพอร์ตี้ได้เช่นกัน

TypeScript
1interface SpecialTypes<T> {
2 [sym: symbol]: (value: T) => void
3}
4
5const 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 (|) ได้อีกด้วย

TypeScript
1interface KeyPair {
2 [key: string | symbol]: unknown
3}

Exact Optional Property Types

ออบเจ็กต์บน JavaScript หากเข้าถึงพรอพเพอร์ตี้ที่ไม่มีจริงเราย่อมได้ค่า undefined กลับออกมา ผลลัพธ์ดังกล่าวเป็นเช่นเดียวกับกรณีที่เข้าถึงพรอพเพอร์ตี้ที่มีค่าเป็น undefined

Code
1const person = {
2 name: 'Somchai',
3 age: 24,
4 tel: undefined,
5}
6
7console.log(person.tel) // undefined
8console.log(person.address) // undefined

เมื่อเป็นเช่นนี้ TypeScript จึงถือว่ากรณีของการใช้ Optional Property ก็ควรอนุญาตให้กำหนด undefined ให้กับมันได้ด้วย พิจารณาพรอพเพอร์ตี้ address ที่เป็น Optional Property ต่อไปนี้

TypeScript
1interface Person {
2 name: string
3 age: number
4 tel: string | undefined
5 address?: string
6}

TypeScript จะถือว่าพรอพเพอร์ตี้ดังกล่าวมีชนิดข้อมูลเดียวกันกับโค้ดข้างล่างที่อนุญาตให้กำหนด undefined ให้กับมันได้โดยตรง

TypeScript
1interface Person {
2 name: string
3 age: number
4 tel: string | undefined
5 address?: string | undefined
6}

กำหนดให้มีตัวแปร person ที่มีชนิดข้อมูลเป็น Person ดังต่อไปนี้

TypeScript
1const person: Person = {
2 name: 'Somchai',
3 age: 24,
4 tel: undefined,
5}

กรณีของโค้ดข้างต้นเรากล่าวได้ว่าทั้ง person.tel และ person.address ต่างได้ค่าเป็น undefined

Code
1console.log(person.tel) // undefined
2console.log(person.address) // undefined

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

Code
1'name' in person // true
2'address' in person // false
3'tel' in person // true

เพื่อให้จำแนกได้อย่างชัดเจน TypeScript จึงไม่ควรกำหนดให้ Optional Property สามารถกำหนด undefined ให้กับมันได้โดยตรง กล่าวคือชนิดข้อมูล Person ข้างต้นนี้เมื่อกำหนดพรอพเพอร์ตี้ address เป็น address?: string ย่อมไม่ควรให้มีความหมายเดียวกับ address?: string | undefined นั่นเอง

TypeScript 4.4 สามารถระบุตัวเลือกขอคอมไพเลอร์ด้วย flag --exactOptionalPropertyTypes ค่านี้เป็นการแจ้ง TypeScript ให้ทราบว่าอย่าเติม | undefined ให้กับ Optional Property

TypeScript
1interface Person {
2 name: string
3 age: number
4 tel: string | undefined
5 address?: string
6}
7
8const person: Person = {
9 name: 'Somchai',
10 age: 24,
11 tel: undefined,
12}
13
14// กรณีไม่ได้เปิดใช้ --exactOptionalPropertyTypes
15// ชนิดข้อมูลเป็น string | undefined
16person.address
17
18// สามารถทำได้
19person.address = undefined
20
21// กรณีไม่ได้เปิดใช้ --exactOptionalPropertyTypes
22// ชนิดข้อมูลเป็น string เท่านั้น
23person.address
24
25// ไม่สามารถทำได้
26person.address = undefined

Static Block ในคลาส

TypeScript 4.4 อนุญาตให้เราสร้าง static block ในคลาสได้ static block เป็นส่วนของบลอคที่จะถูกเรียกครั้งเดียวเมื่อคลาสดังกล่าวถูกโหลดเข้าสู่หน่วยความจำ โดยการทำงานของบลอคนี้จะทำก่อนส่วนของ constructor เสมอ

TypeScript
1class Foo {
2 static #preferences = 0;
3
4 get preferences() {
5 return Foo.#preferences;
6 }
7
8 static {
9 try {
10 const preferences = loadPreferencesFromFile('./preferences.yml');
11
12 Foo.#preferences = preferences;
13 } catch {
14 // handles errors
15 }
16 }
17}

static block ในคลาสไม่จำเป็นต้องมีบลอคเดียว แต่สามารถมีหลายบลอคได้โดยลำดับการทำงานจะเรียงต่อกันจากบนลงล่างตามลำดับโค้ด

การกำหนดชนิดข้อมูล unknown สำหรับตัวแปรใน catch ของประโยค try/catch

ส่วนของบลอค try ในภาษา JavaScript สามารถโยนข้อผิดพลาดด้วยชนิดข้อมูลใด ๆ ก็ได้ เช่น Error หรือ TypeError ด้วยเหตุนี้ตัวแปรสำหรับการรับค่าใน catch TypeScript จึงกำหนดชนิดข้อมูลให้เป็น any

TypeScript
1try {
2 throw new Error('Error Na Ja')
3 // หรือ
4 throw new TypeError('Error Ei Ei')
5
6 // เพื่อให้ ex สามารถรองรับการ throw ที่ต่างชนิดข้อมูลกันได้
7 // ex จึงมีชนิดข้อมูลเป็น any
8} catch (ex) {}

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

TypeScript
1try {
2 throw new Error('Error Na Ja')
3 // หรือ
4 throw new TypeError('Error Ei Ei')
5} catch (ex) {
6 // เรียกใช้ได้ TypeScript ไม่ตรวจสอบเนื่องจาก ex เป็น any
7 console.log(ex.eiei)
8 console.log(ex.naja)
9}

TypeScript 4.4 มีแฟลคใหม่ชื่อ --useUnknownInCatchVariables ที่เมื่อเปิดใช้แล้วจะทำให้ชนิดข้อมูลของตัวแปร ใน catch เป็น unknown กรณีที่เปิดแฟลค --strict ก็จะรวมการทำงานของ --useUnknownInCatchVariables เข้าไว้แล้วด้วยเช่นกัน

TypeScript
1// กำหนดให้เปิดใช้ --useUnknownInCatchVariables
2// หรือ --strict
3try {
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)
9
10 // ทำงานได้อย่างถูกต้อง
11 if (ex instanceof Error) {
12 console.error(ex.message)
13 }
14}

การปรับปรุงการทำงานของ Control Flow สำหรับประโยคเงื่อนไขและ Discriminated Unions

สำหรับภาษา TypeScript type guard คือกระบวนการตรวจสอบชนิดข้อมูลเพื่อจำกัดขอบเขตของชนิดข้อมูลนั้นให้แคบลง โดยอาศัยประโยคเงื่อนไข เช่น if ในการทำงาน

TypeScript
1function squeeze<T extends string | number>(item: T) {
2 // type guard ตรวจสอบ word ก่อนว่าเป็น string หรือไม่
3 if (typeof item === 'string') {
4 // TypeScript จะทราบว่า item เป็น string
5 // จึงสามารถใช้เมธอด replace ของ string ได้
6 return item.replace(/[_\t\n ]/g, '')
7 }
8
9 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)
10}

กรณีที่เราแยกเงื่อนไขการตรวจสอบออกนอกประโยคเงื่อนไข if TypeScript จะไม่สามารถจำแนกได้อีกต่อไปว่า item มีชนิดข้อมูล เป็น string ไม่ใช่ T

TypeScript
1function squeeze<T extends string | number>(item: T) {
2 const isString = typeof item === 'string'
3
4 if (isString) {
5 // Property 'replace' does not exist on type 'T'
6 return item.replace(/[_\t\n ]/g, '')
7 }
8
9 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)
10}

สำหรับ TypeScript 4.4 หากตัวแปรสำหรับการทดสอบนั้นเป็นค่าคงที่ (const), readonly หรือเป็นการประกาศที่ไม่อนุญาตให้ตัวแปรเปลี่ยนแปลงค่าได้ TypeScript จะสามารถอนุมานการทำงานของ type guard นั้นได้อย่างถูกต้อง

TypeScript
1// สำหรับ TypeScript 4.4
2function squeeze<T extends string | number>(item: T) {
3 // เนื่องจากตัวแปรเป็น const
4 const isString = typeof item === 'string'
5
6 // type guard ทำงานได้ถูกต้อง
7 if (isString) {
8 return item.replace(/[_\t\n ]/g, '')
9 }
10
11 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)
12}

หากเปลี่ยนตัวแปร isString ของเราเป็น let ที่อนุญาตให้แก้ไขค่าข้อมูลภายหลังได้ TypeScript จะไม่ทราบว่าก่อนหน้า type guard ตัวแปรจะถูกเปลี่ยนค่าเป็นอย่างอื่นอีกหรือไม่ การทำงานจึงไม่สมบูรณ์

TypeScript
1// สำหรับ TypeScript 4.4
2function squeeze<T extends string | number>(item: T) {
3 // เนื่องจากตัวแปรเป็น let อาจถูกแก้ไขค่าภายหลังได้
4 let isString = typeof item === 'string'
5
6 // type guard ทำงานได้ไม่ถูกต้อง
7 if (isString) {
8 // ยังคงมองเห็น item เป็นชนิดข้อมูล T
9 // Property 'replace' does not exist on type 'string | number'.
10 return item.replace(/[_\t\n ]/g, '')
11 }
12
13 return `${item}`.split('').reduce((acc, ch) => +acc + +ch, 0)
14}

ผลลัพธ์จากการปรับปรุงประสิทธิภาพในการวิเคราะห์ในส่วนนี้ยังส่งผลถึงการทำงานของ Discriminated Union ด้วย ก่อนหน้านี้ถ้าเราต้องการตรวจสอบ Tag ใด ๆ เราไม่สามารถ destructuring ตัวแปร Tag นั้นได้ก่อน

TypeScript
1type InputProps =
2 | { variant: 'input'; capture?: boolean | string | undefined }
3 | { variant: 'textarea'; cols?: number | undefined }
4
5const Input = (props: InputProps) => {
6 const { variant } = props
7
8 if (variant === 'input') {
9 props.capture // error!
10
11 // render input
12 } else {
13 props.cols // error!
14
15 // render textarea
16 }
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 อย่างมืออาชีพ