มีอะไรใหม่บ้างใน TypeScript 4.7
TypeScript ภาษาทางเลือกเพื่อการเพิ่ม Types ให้กับ JavaScript เดินทางมาถึงเวอร์ชัน 4.7 แล้ว รอบนี้มีทั้งการปรับปรุงฟีเจอร์เดิมและเพิ่มของใหม่รวมถึงการเปลี่ยนแปลงแบบ Breaking Changes เหมือนทุกครั้งที่ปล่อยออกมา
การใช้ extends ควบคู่กับ infer
เพื่อให้เห็นประโยชน์ของการใช้ extends ควบคู่กับ infer เรามาลองดูเหตุการณ์สมมติของชนิดข้อมูล StartsWithString
กันครับ
สมมติเราต้องการสร้างชนิดข้อมูล PopNum
ที่พิจารณาอาร์เรย์ว่าช่องแรกของมันว่าเป็นตัวเลขหรือไม่ถ้าใช่จึงคืนตัวเลขนั้นกลับคืน
ผ่านวิธีการเรียกใช้แสนเรียบง่าย ดังนี้
1type Foo = PopNum<[1, 2, 3]>; // 12type Bar = PopNum<['A', 2, 3]>; // never
หลักการสร้าง PopNum นั้นช่างเรียบง่าย เพียงแค่กำหนดเงื่อนไขการตรวจสอบช่องแรกว่าเป็น number หรือไม่ ถ้าใช่ช่องแรกของอาร์เรย์ย่อมถูกคืนกลับ หากไม่แล้ว never จะเป็นค่าสื่อความว่าเงื่อนไขนั้นไม่เป็นจริง
1type PopNum<T extends unknown[]> = T extends [number, ...unknown[]]2 ? T[0]3 : never;
ืืแม้นว่าโค้ดเช่นนี้จะทำงานได้ ทว่าการเรียก T[0]
โดยตรงก็ออกแนวถึกไปหน่อย ลองจินตนาการถึงการสร้างชนิดข้อมูลที่เราไม่ได้สนใจช่องแรก
แต่ข้อมูลนั้นไปอยู่ในช่องที่ 5 การเรียก T[5]
จะทำให้โค้ดดูซับซ้อน เราจะไม่มีทางรู้ว่าช่องที่ 5 มีความหมายว่าอย่างไรจนกว่าจะไล่เรียงลำดับของมัน
ทางออกที่ดีกว่าคือการใช้ Pattern Matching เพื่อเข้าคู่ตำแหน่งที่สนใจกับตัวแปรที่กำหนดขึ้น ดังนี้
1type PopNum<T extends unknown[]> = T extends [infer U, ...unknown[]]2 ? U extends number3 ? U4 : never5 : 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 ได้ ผนวกกับฟีเจอร์นี้ โค้ดรูปแบบใหม่ของเราจึงเป็นดังนี้
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 จึงเกิดขึ้นเพื่อสร้างส่วนประกอบทีละส่วนของหุ่นตนนั้น
1function build<T>(part: T) {2 return {3 part,4 createdAt: new Date(),5 };6}
part นั้นคือส่วนที่ต้องการสร้างโดยมีชนิดข้อมูลเป็น T เมื่อเราต้องการสร้างส่วนหัวจึงต้องกำหนดชนิดข้อมูลของส่วนหัวขึ้นมาก่อน เช่น
1type Head = {};
บางครั้งเราก็อยากทราบว่าฟังก์ชัน build ของเราคืนค่ากลับเป็นชนิดข้อมูลใด เราจึงใช้ ReturnType เพื่อตรวจสอบค่า
1type HeadPart = ReturnType<typeof build>;23// มีค่าเทียบเท่ากับ4// type HeadPart = {5// part: unknown;6// createdAt: Date;7// }
เหตุเพราะส่วนของ part เราไม่ได้กำหนด Generic Parameter Type ค่าที่คืนกลับของ part จึงเป็น unknown หากแต่เราทราบอยู่แล้วว่าเรากำลังจะสร้างส่วนหัวของหุ่นยนต์เราจึงอยากให้ part คืนกลับเป็น Head อาศัยความสามารถของ Generic เราจึงกำหนดค่า Head เป็น Parameter Type ดังนี้
1type HeadPart = ReturnType<typeof build<Head>>23// มีค่าเทียบเท่ากับ4// type HeadPart = {5// part: Head;6// createdAt: Date;7// }
ช่างน่าขายหน้ายิ่งนักที่ TypeScript เวอร์ชันก่อนหน้าไม่สนับสนุนให้ทำสิ่งนี้ แต่... นั่นไม่ใช่สำหรับ TypeScript 4.7 ความสามารถนี้ได้ถูกเพิ่มเข้ามาแล้ว
ไม่ใช่แค่ ReturnType ด้วยความสามารถของ TypeScript 4.7 เราสามารถกำหนดตัวแปรให้เป็น alias ของ Generic Function พร้อมค่า Parameter Type ได้ด้วยเช่นกัน
1const head = build<Head>;23// มีค่าเทียบเท่ากับ4// const head: (part: Head) => {5// part: Head;6// createdAt: Date;7// }
moduleSuffixes
ในบางสถานการณ์ที่เราต้องการตั้งชื่อไฟล์ให้ลงท้ายด้วยนามสกุลอื่นที่ไม่ใช่ .ts
เช่น .ios.ts
หรือ .android.ts
เราสามารถระบุ moduleSuffixes
ในส่วนของ compilerOptions
ของไฟล์ tsconfig.json
ได้ ดังนี้
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 ดังนี้
1interface Data {2 content: string;3}45interface Log extends Data {6 level: 'Fatel' | 'Error' | 'Warn' | 'Info';7}
ต่อมาเรากำหนดชนิดข้อมูล Producer ดังนี้
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
1declare let dataProducer: Producer<Data>;2declare let logProducer: Producer<Log>;34dataProducer = logProducer;56// 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 ดังนี้
1type Consumer<T> = (item: T) => void;
จากนั้นจึงกำหนดค่าของ Consumer<Log>
และ Consumer<Data>
เพื่อพิจารณาความสัมพันธ์
1declare let dataConsumer: Consumer<Data>;2declare let logConsumer: Consumer<Log>;34logConsumer = 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 ได้ดังนี้
1type Producer<out T> = () => T;2type Consumer<in T> = (item: T) => void;
Variant Annotations นั้นช่วยเพิ่มประสิทธิภาพให้กับโครงสร้างข้อมูลที่มีความซับซ้อนสูงนั่นเพราะการประกาศ Variant อย่างชัดแจ้ง ทำให้ TypeScript ลดเวลาในการ infer ชนิดข้อมูลได้ส่วนหนึ่ง นอกจากนี้ Compiler ของภาษาอาจมีการอนุมาน Variant ผิดพลาด ทำให้เราได้ผลลัพธ์ไม่ตรงกับความเป็นจริง
1type Foo<T> = {2 x: T;3 f: Bar<T>;4};56type Bar<U> = (x: Baz<U[]>) => void;78type Baz<V> = {9 value: Foo<V[]>;10};1112declare let foo1: Foo<unknown>;13declare let foo2: Foo<string>;1415foo1 = foo2; // Should be an error but isn't16foo2 = foo1; // Error
จากโค้ดข้างต้นบรรทัดที่ 15 ควรเกิดข้อผิดพลาดแต่ความเป็นจริงไม่เป็นเช่นนั้น นั่นเพราะT ของ Foo จะถูกมองเป็น covariant ทำให้กำหนด foo2 ให้กับตัวแปร foo1 ได้
หากมองให้ลึกลงไปจะเห็นว่าสิ่งนนี้นั้นผิดพลาดเหตุเพราะบรรทัดที่ 6 TypeScript จะอนุมานให้ U เป็น contravariant
แต่บรรทัดที่ 3 กลับมอง T (ซึ่งจะเปลี่ยนเป็น U ใน Bar ภายหลัง) ให้เป็น covariant นั่นทำให้การประมวลผลนี้ดูขัดแย้งกัน
ความเป็นจริงแล้ว T ของ Foo ควรเป็น invariant เราจึงต้องกำหนด in out
ให้กับ T เพื่อยังผลลัพธ์ให้กลับมาถูกต้องอีกครั้ง
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
1{2 "compilerOptions": {3 "module": "node16"4 }5}
ธรรมชาติของ Node.js เมื่อเราระบุนามสกุลไฟล์ระบบ Module ที่ใช้จะถือเป็น CJS เสมอเว้นเสียแต่เราจะกำหนด module เป็น ESM
ใน package.json
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 ใด ๆ จะต้องระบุนามสกุลไฟล์ด้วยเสมอ
1// ใช้ได้เฉพาะกับ CJS เนื่องจากไม่ระบุนามสกุลไฟล์2import AppBar from './ui/components/AppBar';34// ใช้ได้กับทั้ง CJS และ ESM5import 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 เช่น
1{2 "main": "./commonjs/index.cjs"3}
หากโปรเจคของเรามีทั้งการใช้งาน CJS และ ESM เราสามารถบอก Node.js ได้ว่าจุดเริ่มต้นของระบบ Module ทั้งสองอยู่ที่ใดด้วยการใช้ exports
1{2 "type": "module",3 "exports": {4 ".": {5 // Entry-point สำหรับ `import "my-package"` ใน ESM6 "import": "./esm/index.js",78 // Entry-point สำหรับ `require("my-package") ใน CJS9 "require": "./commonjs/index.cjs"10 }11 },1213 // 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 ได้
1{2 "name": "my-package",3 "type": "module",4 "exports": {5 ".": {6 // Entry-point สำหรับ `import "my-package"` ใน ESM7 "import": {8 // Declaration file สำหรับ TypeScript9 "types": "./types/esm/index.d.ts",1011 // ไฟล์ Entry Point ที่ Node.js จะนำไปใช้ต่อ12 "default": "./esm/index.js"13 },14 // Entry-point สำหรับ `require("my-package") ใน CJS15 "require": {16 // Declaration file สำหรับ TypeScript17 "types": "./types/commonjs/index.d.cts",1819 // ไฟล์ Entry Point ที่ Node.js จะนำไปใช้ต่อ20 "default": "./commonjs/index.cjs"21 }22 }23 },2425 // Fall-back สำหรับ TypeScript เวอร์ชันเก่า26 "types": "./types/index.d.ts",2728 // CJS fall-back สำหรับ Node.js เวอร์ชันเก่า29 "main": "./commonjs/index.cjs"30}
ปรับปรุงการอนุมานค่าข้อมูลของ Computed Property
ก่อนหน้านี้ TypeScript ไม่สามารถอนุมานค่าของ Computed Property ที่มีค่า Indexed Key เป็น Literal Types หรือ Unique Symbol ได้อย่างถูกต้อง
1const key = Symbol();23const obj = {4 [key]: Math.random() < 0.5 ? 8 : 'hello',5};67if (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 แล้วหรือไม่
1const key = Symbol();23class C {4 // Property '[key]' has no initializer and is not definitely assigned in the constructor.5 [key]: string;67 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 อย่างมืออาชีพ
- สรุป