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

Nuttavut Thongjor

TypeScript ภาษาทางเลือกเพื่อการเพิ่ม Types ให้กับ JavaScript เดินทางมาถึงเวอร์ชัน 4.7 แล้ว รอบนี้มีทั้งการปรับปรุงฟีเจอร์เดิมและเพิ่มของใหม่รวมถึงการเปลี่ยนแปลงแบบ Breaking Changes เหมือนทุกครั้งที่ปล่อยออกมา

การใช้ extends ควบคู่กับ infer

เพื่อให้เห็นประโยชน์ของการใช้ extends ควบคู่กับ infer เรามาลองดูเหตุการณ์สมมติของชนิดข้อมูล StartsWithString กันครับ

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

TypeScript
1type Foo = PopNum<[1, 2, 3]>; // 1
2type Bar = PopNum<['A', 2, 3]>; // never

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

TypeScript
1type PopNum<T extends unknown[]> = T extends [number, ...unknown[]]
2 ? T[0]
3 : never;

ืืแม้นว่าโค้ดเช่นนี้จะทำงานได้ ทว่าการเรียก T[0] โดยตรงก็ออกแนวถึกไปหน่อย ลองจินตนาการถึงการสร้างชนิดข้อมูลที่เราไม่ได้สนใจช่องแรก แต่ข้อมูลนั้นไปอยู่ในช่องที่ 5 การเรียก T[5] จะทำให้โค้ดดูซับซ้อน เราจะไม่มีทางรู้ว่าช่องที่ 5 มีความหมายว่าอย่างไรจนกว่าจะไล่เรียงลำดับของมัน ทางออกที่ดีกว่าคือการใช้ Pattern Matching เพื่อเข้าคู่ตำแหน่งที่สนใจกับตัวแปรที่กำหนดขึ้น ดังนี้

TypeScript
1type PopNum<T extends unknown[]> = T extends [infer U, ...unknown[]]
2 ? U extends number
3 ? U
4 : never
5 : never;

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

เมื่อเปรียบเทียบโค้ดทั้งสองย่อมพบว่าโค้ดชุดหลังต้องเขียนเยอะกว่า ยิ่งเจอกับเครื่องหมาย ? หลายชั้นยิ่งงงงวยเข้าไปใหญ่ สาเหตุที่เป็นเช่นนี้นั่นเพราะ ในจังหวะของการเรียก T extends [infer U, ...unknown[]] นั้น เรายังไม่ทราบชนิดข้อมูลของ U จึงต้องมีเงื่อนไขคือ U extends number อีกครั้งเพื่อใช้ตรวจสอบชนิดของ U ว่าเป็นตัวเลขหรือไม่

การจะแก้ไขปัญหานี้ได้เราต้องกำจัดขั้นตอนที่ซับซ้อนด้วยการทำค่า U ให้ชัดแจ้งตั้งแต่ตอน infer ว่าชนิดข้อมูลที่กำลังพิจารณาคือค่าใด

TypeScript 4.7 ทำให้ทุก Conditional Types ที่มีการ infer ค่า สามารถระบุ constraint หลัง extends ได้ ผนวกกับฟีเจอร์นี้ โค้ดรูปแบบใหม่ของเราจึงเป็นดังนี้

TypeScript
1type PopNum<T extends unknown[]> =
2 T extends [infer U extends number, ...unknown[]] ? U : never;

Instantiation Expressions

การสร้างตัวแปลเพื่อชี้ไปยัง Generic Function พร้อมกำหนดส่วนของ Parameter Type นั้นไม่สามารถกระทำได้มาก่อนจนกระทั่งการมาถึงของ TypeScript 4.7

สมมติเราต้องการสร้างหุ่นยนต์ที่มีส่วนประกอบของร่างกาย เช่น ส่วนหัว เป็นต้น เพื่อให้การนี้สำเร็จฟังก์ชัน build จึงเกิดขึ้นเพื่อสร้างส่วนประกอบทีละส่วนของหุ่นตนนั้น

TypeScript
1function build<T>(part: T) {
2 return {
3 part,
4 createdAt: new Date(),
5 };
6}

part นั้นคือส่วนที่ต้องการสร้างโดยมีชนิดข้อมูลเป็น T เมื่อเราต้องการสร้างส่วนหัวจึงต้องกำหนดชนิดข้อมูลของส่วนหัวขึ้นมาก่อน เช่น

TypeScript
1type Head = {};

บางครั้งเราก็อยากทราบว่าฟังก์ชัน build ของเราคืนค่ากลับเป็นชนิดข้อมูลใด เราจึงใช้ ReturnType เพื่อตรวจสอบค่า

TypeScript
1type HeadPart = ReturnType<typeof build>;
2
3// มีค่าเทียบเท่ากับ
4// type HeadPart = {
5// part: unknown;
6// createdAt: Date;
7// }

เหตุเพราะส่วนของ part เราไม่ได้กำหนด Generic Parameter Type ค่าที่คืนกลับของ part จึงเป็น unknown หากแต่เราทราบอยู่แล้วว่าเรากำลังจะสร้างส่วนหัวของหุ่นยนต์เราจึงอยากให้ part คืนกลับเป็น Head อาศัยความสามารถของ Generic เราจึงกำหนดค่า Head เป็น Parameter Type ดังนี้

TypeScript
1type HeadPart = ReturnType<typeof build<Head>>
2
3// มีค่าเทียบเท่ากับ
4// type HeadPart = {
5// part: Head;
6// createdAt: Date;
7// }

ช่างน่าขายหน้ายิ่งนักที่ TypeScript เวอร์ชันก่อนหน้าไม่สนับสนุนให้ทำสิ่งนี้ แต่... นั่นไม่ใช่สำหรับ TypeScript 4.7 ความสามารถนี้ได้ถูกเพิ่มเข้ามาแล้ว

ไม่ใช่แค่ ReturnType ด้วยความสามารถของ TypeScript 4.7 เราสามารถกำหนดตัวแปรให้เป็น alias ของ Generic Function พร้อมค่า Parameter Type ได้ด้วยเช่นกัน

TypeScript
1const head = build<Head>;
2
3// มีค่าเทียบเท่ากับ
4// const head: (part: Head) => {
5// part: Head;
6// createdAt: Date;
7// }

moduleSuffixes

ในบางสถานการณ์ที่เราต้องการตั้งชื่อไฟล์ให้ลงท้ายด้วยนามสกุลอื่นที่ไม่ใช่ .ts เช่น .ios.ts หรือ .android.ts เราสามารถระบุ moduleSuffixes ในส่วนของ compilerOptions ของไฟล์ tsconfig.json ได้ ดังนี้

Code
1{
2 "compilerOptions": {
3 "moduleSuffixes": [".ios", ".andoid", ""]
4 }
5}

หากเราเขียนประโยค import เช่น import AppBar from "./AppBar"; จากการตั้งค่าดังกล่าว TypeScript จะพยายามมองหาไฟล์ชื่อ ./AppBar.ios.ts ./AppBar.android.ts และ ./AppBar.ts ตามลำดับ ทั้งนี้ moduleSuffixes จำเป็นต้องระบุค่าว่าง ("") เสมอเพื่อเป็นตัวแทนของการมองหาไฟล์นามสกุล .ts ตามปกติ

Variant Annotations สำหรับ Type Parameters

Variant Annotions เป็น modifier ตัวหนึ่งที่ใช้กับ Type Paramers ของ Generic เพื่อกำหนดความสามารถของสิ่งที่เรียกว่า variance โดยใช้ out และ in แทน covariant และ contravariant ตามลำดับ ฟีเจอร์นี้มีใช้อย่างแพร่พลายจากภาษาอื่น เช่น C# หรือ Kotlin

เพื่อให้เข้าใจนิยามของ Variant เราจะเริ่มต้นหัวข้อนี้ด้วยการกำหนดนิยามของสิ่งนี้กันก่อนครับ

สมมติให้ interface ทั้งสองคือ Data และ Log มีความสัมพันธ์กันโดย Log เป็น subtype ของ Data ดังนี้

TypeScript
1interface Data {
2 content: string;
3}
4
5interface Log extends Data {
6 level: 'Fatel' | 'Error' | 'Warn' | 'Info';
7}

ต่อมาเรากำหนดชนิดข้อมูล Producer ดังนี้

TypeScript
1type Producer<T> = () => T;

เราทราบกันดีว่า Log เป็น subtype ของ Data (แทนด้วยสัญลักษณ์ Log -> Data) แต่ถ้าแทนที่ Type Parameter T ด้วย Log และ Data เกิดเป็น Producer<Log> และ Producer<Data> หละ ตัวใดจะเป็น subtype ของตัวใดกัน?

เมื่อ Log -> Data ถูกพิสูจน์ได้ว่า Producer<Log> -> Producer<Data> เช่นกัน เรากล่าวได้ว่า T เป็น covariant

TypeScript
1declare let dataProducer: Producer<Data>;
2declare let logProducer: Producer<Log>;
3
4dataProducer = logProducer;
5
6// Type 'Producer<Data>' is not assignable to type 'Producer<Log>'.
7// Property 'level' is missing in type 'Data' but required in type 'Log'.
8logProducer = dataProducer;

จากตัวอย่างโค้ดข้างต้น Producer<Log> -> Producer<Data> นั่นเพราะเรากำหนด logProducer ให้กับ dataProducer ได้ สาเหตุที่เป็นเช่นนี้เพราะ Producer เป็นฟังก์ชันที่คืน T เราทราบอยู่แล้วว่า dataProducer จะต้องคืน T เป็น Data ที่ต้องมีฟิลด์คือ content เหตุเพราะ logProducer คืน T เป็น Log ที่ตัวมันเองก็ประกอบด้วยฟิลด์ content เช่นกัน ความที่ TypeScript เป็นภาษาประเภท Structural Type System คือพิจารณาความเข้ากันได้ของข้อมูลจากโครงสร้างนั่นจึงเป็นเหตุผลที่ Producer<Log> -> Producer<Data> จากข้อสรุปนี้จึงกล่าวได้ว่า T ของ Producer เป็น covariant

ไม่ใช่สำหรับประโยค logProducer = dataProducer ที่จะเกิดข้อผิดพลาดนั่นเพราะ logProducer ต้องคืนค่า T เป็น Log ที่มีฟิลด์คือ level เมื่อเรานำ dataProducer ที่คืน T เป็น Data แบบไม่มีฟิลด์คือ level ไปกำหนดค่าให้ TypeScript จึงยอมรับความเข้ากันไม่ได้ของโครงสร้างข้อมูลที่แตกต่างกันนี้

เรามาลองกำหนดชนิดข้อมูลของ Consumer ดังนี้

TypeScript
1type Consumer<T> = (item: T) => void;

จากนั้นจึงกำหนดค่าของ Consumer<Log> และ Consumer<Data> เพื่อพิจารณาความสัมพันธ์

TypeScript
1declare let dataConsumer: Consumer<Data>;
2declare let logConsumer: Consumer<Log>;
3
4logConsumer = dataConsumer;
5// Type 'Consumer<Log>' is not assignable to type 'Consumer<Data>'.
6// Property 'level' is missing in type 'Data' but required in type 'Log'.
7dataConsumer = logConsumer;

จากความสัมพันธ์นี้พบว่าเมื่อ Log -> Data แต่ความสัมพันธ์ของ T ใน Consumer กลับด้านเป็น Consumer<Data> -> Consumer<Log> เราเรียก T นี้ว่าเป็น contravariant

กรณีที่ Log -> Data แต่ Consumer<Log> และ Consumer<Data> ต่างไม่เป็น subtype ของกันและกัน T ในที่นี้จะเป็น invariant

TypeScript 4.7 ได้เพิ่ม in และ out เพื่อให้เราสามารถกำหนด variant ได้ตามความต้องการ โดย out ใช้กับ covariant ในขณะที่ in ใช้กำหนด contravariant ทั้งนี้เราสามารถระบุเป็น in out เพื่อสร้างความสัมพันธ์แบบ invariant ได้

จากโค้ดข้างต้นพบว่า T ของ Producer เป็น covariant และ T ของ Consumer เป็น contravariant ทั้งสองนี้เกิดขึ้นอย่างอัตโนมัติจากการอนุมานของ TypeScript แต่หากเราต้องการระบุความสัมพันธ์อย่างชัดแจ้งเราสามารถกำหนด in และ out ได้ดังนี้

TypeScript
1type Producer<out T> = () => T;
2type Consumer<in T> = (item: T) => void;

Variant Annotations นั้นช่วยเพิ่มประสิทธิภาพให้กับโครงสร้างข้อมูลที่มีความซับซ้อนสูงนั่นเพราะการประกาศ Variant อย่างชัดแจ้ง ทำให้ TypeScript ลดเวลาในการ infer ชนิดข้อมูลได้ส่วนหนึ่ง นอกจากนี้ Compiler ของภาษาอาจมีการอนุมาน Variant ผิดพลาด ทำให้เราได้ผลลัพธ์ไม่ตรงกับความเป็นจริง

TypeScript
1type Foo<T> = {
2 x: T;
3 f: Bar<T>;
4};
5
6type Bar<U> = (x: Baz<U[]>) => void;
7
8type Baz<V> = {
9 value: Foo<V[]>;
10};
11
12declare let foo1: Foo<unknown>;
13declare let foo2: Foo<string>;
14
15foo1 = foo2; // Should be an error but isn't
16foo2 = foo1; // Error

จากโค้ดข้างต้นบรรทัดที่ 15 ควรเกิดข้อผิดพลาดแต่ความเป็นจริงไม่เป็นเช่นนั้น นั่นเพราะT ของ Foo จะถูกมองเป็น covariant ทำให้กำหนด foo2 ให้กับตัวแปร foo1 ได้

หากมองให้ลึกลงไปจะเห็นว่าสิ่งนนี้นั้นผิดพลาดเหตุเพราะบรรทัดที่ 6 TypeScript จะอนุมานให้ U เป็น contravariant แต่บรรทัดที่ 3 กลับมอง T (ซึ่งจะเปลี่ยนเป็น U ใน Bar ภายหลัง) ให้เป็น covariant นั่นทำให้การประมวลผลนี้ดูขัดแย้งกัน ความเป็นจริงแล้ว T ของ Foo ควรเป็น invariant เราจึงต้องกำหนด in out ให้กับ T เพื่อยังผลลัพธ์ให้กลับมาถูกต้องอีกครั้ง

TypeScript
1type Foo<in out T> = {
2 x: T;
3 f: Bar<T>;
4}

ES Module และนามสกุลไฟล์

ปัจจุบัน Node.js ได้สนับสนุนระบบ Module 2 รูปแบบได้แก่ CommonJS (CJS) ที่เป็นระบบเดิมของ Node.js อยู่แล้ว กับระบบใหม่คือ ECMAScript modules (ESM) ที่สนับสนุนการใช้งานตั้งแต่ Node.js เวอร์ชัน 12

TypeScript 4.7 ได้เพิ่มการตั้งค่า module ในส่วนของ compilerOptions เมื่อระบุเป็น node16 และ nodenext TypeScript จะสนับสนุนรูปแบบการทำงานของทั้ง CJS และ ESM

Code
1{
2 "compilerOptions": {
3 "module": "node16"
4 }
5}

ธรรมชาติของ Node.js เมื่อเราระบุนามสกุลไฟล์ระบบ Module ที่ใช้จะถือเป็น CJS เสมอเว้นเสียแต่เราจะกำหนด module เป็น ESM ใน package.json

Code
1{
2 "type": "module"
3}

เมื่อเรากำหนด type เป็น module ไฟล์ .js ใด ๆ จะถูกจัดการด้วยระบบของ ESM ผลกระทบดังกล่าวนั้นรวมถึง

  • ไม่สามารถใช้ require/module จาก CJS ได้โดยตรงแต่ใช้ import/export ตามระบบ ESM แทน
  • สามารถใช้ await นอก async ฟังก์ชัน ได้ (Top-level await)
  • การ import ไฟล์ต้องระบุนามสกุลไฟล์ด้วยเสมอ

แต่เดิมไฟล์ TypeScript (นามสกุล .ts และ .tsx) เมื่อถูกคอมไพล์เพื่อแปลงเป็น JavaScript การแปลงนี้จะนำไปสู่ผลลัพธ์ที่เป็น CJS แต่เมื่อมีการระบุ type ใน package.json TypeScript จะพิจารณาผลลัพธ์ของการแปลงไฟล์ตามแต่ชนิดที่ระบุ เช่น เมื่อระบุ type เป็น module ผลลัพธ์ของ JavaScript จากการคอมไพล์จะได้ผลลัพธ์ในระบบ ESM

นอกเหนือจากเรื่องผลลัพธ์ของการคอมไพล์ การใช้ ESM ควบคู่กับ TypeScript ต้องปรับเปลี่ยนรูปแบบ import ให้สอดคล้องกับ ESM ของ Node.js นั่นคือประโยค import ใด ๆ จะต้องระบุนามสกุลไฟล์ด้วยเสมอ

TypeScript
1// ใช้ได้เฉพาะกับ CJS เนื่องจากไม่ระบุนามสกุลไฟล์
2import AppBar from './ui/components/AppBar';
3
4// ใช้ได้กับทั้ง CJS และ ESM
5import AppBar from './ui/components/AppBar.js';

การตั้งค่า type ใน package.json จะเป็นการระบุระบบ Module เพื่อใช้ทั้งโปรเจค หากเราต้องการปรับเปลี่ยน Module ที่แตกต่างกันในแต่ละไฟล์เราสามารถกำหนดนามสกุลไฟล์เป็น .cjs เพื่อใช้งานระบบ CJS ในไฟล์นั้น และ .mjs เพื่อใช้ระบบ ESM สำหรับภาษา TypeScript เรากำหนดระบบ Module ที่แตกต่างกันในแต่ละไฟล์ด้วยนามสกุล .cts สำหรับ CJS และ .mts สำหรับ ESM ไม่ใช่เฉพาะไฟล์ TypeScript ปกติ แต่ไฟล์ Declaration ยังสามารถถูกคอมไพล์ด้วยนามสกุลที่แตกต่างกันคือ .d.mts และ .d.cjs สำหรับไฟล์ .mts และ .cjs ตามลำดับ

การปรากฎของ ESM ใน Node.js ย่อมทำให้เกิดความสับสนกับระบบ Module แบบเก่าที่ใช้ในไฟล์สคริปต์เดิม ๆ ได้ Node.js แก้ไขปัญหานี้หากมีการใช้ ESM ไฟล์ entry point ต้องเป็น .mjs หรือไม่เช่นนั้นก็ต้องระบุ ""type": "module" ในไฟล์ package.json

สำหรับภาษา TypeScript นั้นหากไฟล์ใดมีการใช้ประโยค import/export จะถือว่าไฟล์สคริปต์นั้นอยู่บนระบบของ ESM กรณีอื่นจะถือว่าไฟล์เหล่านั้นเป็นสคริปต์ที่ทำงานบนขอบเขตแบบ Global

อย่างไรก็ตาม TypeScript มีความสามารถบางอย่างที่มิอาจพิจารณาประโยค import/export จากโค้ดในไฟล์ได้ เช่นการใช้ --jsx react-jsx ทำให้โค้ด React ไม่ต้องเขียนประโยค import React from 'react' ส่วนนี้ถ้าในไฟล์ไม่มีประโยค import/export เลย ไฟล์นั้นอาจไม่ถูกพิจารณาให้เป็น ESM แท้ที่จริงแล้ว --jsx react-jsx จะทำการเพิ่มประโยค import ของ React ให้อัตโนมัติ

TypeScript 4.7 มีการตั้งค่าแบบใหม่คือ moduleDetection. moduleDetection ที่สามารถระบุค่าได้สามรูปแบบ

  • auto: การใช้งานในโหมดนี้ TypeScript จะทำการตรวจสอบว่าเป็น ESM หรือไม่โดยพิจารณาจากปัจจัยต่าง ๆ คือ ในไฟล์มีประโยค import/export หรือไม่ หรือกรณีเปิดใช้งาน --module nodenext/--module node16 ใน package.json มีการระบุ type เป็น module หรือไม่ หรือหากไฟล์นั้นเป็น JSX ได้มีการระบุ --jsx react-jsx ไว้หรือไม่
  • legacy: ประพฤติแบบเดียวกับ TypeScript 4.6
  • force: การตั้งค่านี้จะทำให้ทุกไฟล์ถูกปฏิบัติในฐานะ Module

Exports และ Imports ใน package.json

โดยทั่วไปโปรเจคของ Node.js จะพิจารณาจุดเริ่มต้นของโปรเจคได้จาก main ใน package.json ซึ่งเป็นส่วนการกำหนดไฟล์ CJS เช่น

Code
1{
2 "main": "./commonjs/index.cjs"
3}

หากโปรเจคของเรามีทั้งการใช้งาน CJS และ ESM เราสามารถบอก Node.js ได้ว่าจุดเริ่มต้นของระบบ Module ทั้งสองอยู่ที่ใดด้วยการใช้ exports

Code
1{
2 "type": "module",
3 "exports": {
4 ".": {
5 // Entry-point สำหรับ `import "my-package"` ใน ESM
6 "import": "./esm/index.js",
7
8 // Entry-point สำหรับ `require("my-package") ใน CJS
9 "require": "./commonjs/index.cjs"
10 }
11 },
12
13 // CJS fall-back สำหรับ Node.js เวอร์ชันเก่า ๆ
14 "main": "./commonjs/index.cjs"
15}

กรณีของภาษา TypeScript เมื่อเรากำหนด main เป็น ./lib/index.js TypeScript จะมองหาไฟล์ Declaration จาก ./lib/index.d.ts กรณีที่ต้องการเปลี่ยนแปลงที่ตั้งของไฟล์นี้ให้ระบุผ่าน types เช่น "types": "./types/index.d.ts" เมื่อเปิดใช้งาน module กับ TypeScript 4.7 เราสามารถใช้ imports เพื่อแบ่งแยกทั้ง Entry Point และไฟล์ Declaration ของทั้ง CJS และ ESM ได้

Code
1{
2 "name": "my-package",
3 "type": "module",
4 "exports": {
5 ".": {
6 // Entry-point สำหรับ `import "my-package"` ใน ESM
7 "import": {
8 // Declaration file สำหรับ TypeScript
9 "types": "./types/esm/index.d.ts",
10
11 // ไฟล์ Entry Point ที่ Node.js จะนำไปใช้ต่อ
12 "default": "./esm/index.js"
13 },
14 // Entry-point สำหรับ `require("my-package") ใน CJS
15 "require": {
16 // Declaration file สำหรับ TypeScript
17 "types": "./types/commonjs/index.d.cts",
18
19 // ไฟล์ Entry Point ที่ Node.js จะนำไปใช้ต่อ
20 "default": "./commonjs/index.cjs"
21 }
22 }
23 },
24
25 // Fall-back สำหรับ TypeScript เวอร์ชันเก่า
26 "types": "./types/index.d.ts",
27
28 // CJS fall-back สำหรับ Node.js เวอร์ชันเก่า
29 "main": "./commonjs/index.cjs"
30}

ปรับปรุงการอนุมานค่าข้อมูลของ Computed Property

ก่อนหน้านี้ TypeScript ไม่สามารถอนุมานค่าของ Computed Property ที่มีค่า Indexed Key เป็น Literal Types หรือ Unique Symbol ได้อย่างถูกต้อง

TypeScript
1const key = Symbol();
2
3const obj = {
4 [key]: Math.random() < 0.5 ? 8 : 'hello',
5};
6
7if (typeof obj[key] === 'string') {
8 obj[key].toUpperCase();
9}

จากโค้ดข้างต้นเมื่อคอมไพล์ด้วย TypeScript 4.6 จะเกิดข้อผิดพลาดขึ้นเนื่องจาก TypeScript ไม่สามารถอนุมานได้อย่างถูกต้อง จึงเข้าใจว่าภายใต้การตรวจสอบของ if ค่า obj[key] ยังคงเป็นได้ทั้ง number และ string อยู่ อย่างไรก็ตามข้อผิดพลาดนี้ได้ถูกแก้ไขเป็นที่เรียบร้อยแล้วใน TypeScript 4.7

เมื่อปัญหาดังกล่าวได้รับการแก้ไข เมื่อตั้งค่า --strictPropertyInitialization จะยังผลให้ TypeScript สามารถตรวจสอบ Computed Properties ในคลาสได้ว่าได้รับการกำหนดค่าใน constructor แล้วหรือไม่

TypeScript
1const key = Symbol();
2
3class C {
4 // Property '[key]' has no initializer and is not definitely assigned in the constructor.
5 [key]: string;
6
7 constructor(str: string) {
8 // oops, forgot to set 'this[key]'
9 }
10}

เรียนรู้ TypeScript อย่างมืออาชีพ

คอร์สออนไลน์ Comprehensive TypeScript คอร์สสอนการใช้งาน TypeScript ตั้งแต่เริ่มต้นจนถึงขั้นสูง เรียนรู้หลักการทำงานของ TypeScript การประกาศชนิดข้อมูลต่าง ๆ พร้อมการใช้งานขั้นสูงพร้อมรองรับการทำงานกับ TypeScript เวอร์ชัน 4.7 ด้วย

สรุป

TypeScript 4.7 ยังมาพร้อมความสามารถอื่นอีกมากมาย เช่น การปรับปรุงการอนุมานเมธอดของออบเจ็กต์, resolution-mode, Group-Aware Organize Imports รวมถึง Breaking Changes ต่าง ๆ ผู้อ่านสามารถศึกษาความสามารถใหม่ ๆ ของ TypeScript 4.7 เพิ่มเติมได้จาก Announcing TypeScript 4.7

สารบัญ

สารบัญ

  • การใช้ extends ควบคู่กับ infer
  • Instantiation Expressions
  • moduleSuffixes
  • Variant Annotations สำหรับ Type Parameters
  • ES Module และนามสกุลไฟล์
  • Exports และ Imports ใน package.json
  • ปรับปรุงการอนุมานค่าข้อมูลของ Computed Property
  • เรียนรู้ TypeScript อย่างมืออาชีพ
  • สรุป