Phân tích control flow và thu hẹp kiểu bằng type guard
TypeScript có thể thu hẹp kiểu của biến theo luồng xử lý thông qua control flow và type guard.
Union type và sự mơ hồ
Khi viết type annotation cho biến bằng union type, sẽ xảy ra lỗi kiểu nếu truy cập vào method hoặc property chỉ được định nghĩa ở một trong các kiểu.
tsfunctionshowMonth (month : string | number) {Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.2339Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.console .log (month .(2, "0")); padStart }
tsfunctionshowMonth (month : string | number) {Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.2339Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.console .log (month .(2, "0")); padStart }
Điều này là do biến month có thể là kiểu string hoặc number, và có nguy cơ xảy ra truy cập vào method chưa được định nghĩa khi kiểu number được truyền vào.
Phân tích control flow
TypeScript phân tích control flow như if hoặc vòng lặp for để xác định khả năng về kiểu tại thời điểm code được thực thi.
Trong ví dụ trước, bằng cách thêm điều kiện kiểm tra biến month là kiểu string, TypeScript sẽ xác định rằng tại thời điểm thực thi method padStart của month, month là kiểu string, giúp giải quyết lỗi kiểu.
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
Hãy xem một ví dụ phức tạp hơn.
Trong ví dụ sau, việc gọi method toFixed của month nằm ngoài scope của điều kiện phân nhánh, nên kiểu của biến month vẫn là string | number, dẫn đến lỗi kiểu.
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.2339Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.console .log (month .()); toFixed }
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.2339Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.console .log (month .()); toFixed }
Thêm return vào điều kiện phân nhánh đầu tiên của hàm để kết thúc xử lý hàm bằng early return.
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));return;}console .log (month .toFixed ());}
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));return;}console .log (month .toFixed ());}
Thay đổi này giải quyết lỗi kiểu khi gọi method toFixed của month.
Điều này là do phân tích control flow xác định rằng nếu biến month là kiểu string, hàm sẽ kết thúc bằng early return, và tại thời điểm method toFixed của month được thực thi, TypeScript xác định biến month chỉ có thể là kiểu number.
Type guard
Trong phần giải thích về control flow, chúng ta đã thu hẹp kiểu bằng cách sử dụng điều kiện if(typeof month === "string") để kiểm tra kiểu của biến và tránh sự mơ hồ về kiểu.
Code kiểm tra kiểu như thế này được gọi là type guard.
typeof
Ví dụ điển hình là type guard sử dụng toán tử typeof.
📄️ Toán tử typeof
Toán tử typeof của JavaScript cho phép kiểm tra kiểu của giá trị.
Trong ví dụ sau, typeof được sử dụng để xác định biến month là kiểu string.
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
tsfunctionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
Cần lưu ý rằng với type guard bằng typeof, typeof null === "object".
Trong JavaScript, null là object, nên khi viết type guard như sau, biến date sẽ bị thu hẹp thành Date | null, vẫn còn khả năng là null, dẫn đến lỗi kiểu.
tsfunctiongetMonth (date : string |Date | null) {if (typeofdate === "object") {'date' is possibly 'null'.18047'date' is possibly 'null'.console .log (. date getMonth () + 1);}}
tsfunctiongetMonth (date : string |Date | null) {if (typeofdate === "object") {'date' is possibly 'null'.18047'date' is possibly 'null'.console .log (. date getMonth () + 1);}}
Có thể giải quyết lỗi kiểu bằng cách thêm type guard date != null.
tsfunctiongetMonth (date : string |Date | null) {if (typeofdate === "object" &&date != null) {console .log (date .getMonth () + 1);}}
tsfunctiongetMonth (date : string |Date | null) {if (typeofdate === "object" &&date != null) {console .log (date .getMonth () + 1);}}
instanceof
Khi xác định instance bằng typeof, chỉ có thể xác định là object.
Để viết type guard xác định là instance của một class cụ thể, sử dụng instanceof.
tsfunctiongetMonth (date : string |Date ) {if (date instanceofDate ) {console .log (date .getMonth () + 1);}}
tsfunctiongetMonth (date : string |Date ) {if (date instanceofDate ) {console .log (date .getMonth () + 1);}}
in
Không cần chỉ định rõ là instance của class cụ thể, mà có thể thu hẹp kiểu bằng cách viết type guard sử dụng toán tử in để kiểm tra object có property cụ thể hay không.
tsinterfaceWizard {castMagic (): void;}interfaceSwordsman {slashSword (): void;}functionattack (player :Wizard |Swordsman ) {if ("castMagic" inplayer ) {player .castMagic ();} else {player .slashSword ();}}
tsinterfaceWizard {castMagic (): void;}interfaceSwordsman {slashSword (): void;}functionattack (player :Wizard |Swordsman ) {if ("castMagic" inplayer ) {player .castMagic ();} else {player .slashSword ();}}
Hàm type guard do người dùng định nghĩa
Type guard có thể được định nghĩa dưới dạng hàm ngoài việc viết inline.
tsfunction isWizard(player: Player): player is Wizard {return "castMagic" in player;}function attack(player: Wizard | Swordsman) {if (isWizard(player)) {player.castMagic();} else {player.slashSword();}}
tsfunction isWizard(player: Player): player is Wizard {return "castMagic" in player;}function attack(player: Wizard | Swordsman) {if (isWizard(player)) {player.castMagic();} else {player.slashSword();}}
Tên gọi này (user-defined type guard) dường như dài ngay cả trong tiếng Anh, nên đôi khi được gọi là hàm type guard (type guarding function, guard's function).
📄️ Type guard function
Compiler của TypeScript phân tích type của biến tại mỗi vị trí trong control flow như if hay switch, tính năng này được gọi là control flow analysis (phân tích luồng điều khiển).
Gán type guard vào biến
Cũng có thể sử dụng biến cho type guard.
tsfunctiongetMonth (date : string |Date ) {constisDate =date instanceofDate ;if (isDate ) {console .log (date .getMonth () + 1);}}
tsfunctiongetMonth (date : string |Date ) {constisDate =date instanceofDate ;if (isDate ) {console .log (date .getMonth () + 1);}}
Thu hẹp kiểu bằng switch (true)
Câu lệnh switch thực thi code khác nhau dựa trên giá trị của mệnh đề case. Thông thường, trong mệnh đề case sẽ chỉ định chuỗi hoặc số, nhưng trong TypeScript, sử dụng switch (true) cho phép đánh giá biểu thức trả về giá trị boolean trong mỗi mệnh đề case. Trong block case được đánh giá là true, kiểu sẽ tự động được thu hẹp dựa trên điều kiện đó.
tsfunctionhandleValue (value : string | number | boolean): void {switch (true) {case typeofvalue === "string":console .log (`String value: ${value .padStart (2, "0")}`);break;case typeofvalue === "number":console .log (`Number value: ${value .toFixed (2)}`);break;case typeofvalue === "boolean":console .log (`Boolean value: ${value }`);break;default:console .log ("Unknown type");}}
tsfunctionhandleValue (value : string | number | boolean): void {switch (true) {case typeofvalue === "string":console .log (`String value: ${value .padStart (2, "0")}`);break;case typeofvalue === "number":console .log (`Number value: ${value .toFixed (2)}`);break;case typeofvalue === "boolean":console .log (`Boolean value: ${value }`);break;default:console .log ("Unknown type");}}
Không chỉ typeof, mà cũng có thể sử dụng tương tự với instanceof. Trong ví dụ sau, UserError và SystemError là các class có property riêng user và system. Sử dụng switch (true) để phân biệt error nào và truy cập vào property tương ứng.
tsfunctionhandleError (error :UserError |SystemError ): void {switch (true) {caseerror instanceofUserError :console .log (`User error for ${error .user }: ${error .message }`);break;caseerror instanceofSystemError :console .log (`System error for ${error .system }: ${error .message }`);break;default:console .log ("Unknown error type");}}
tsfunctionhandleError (error :UserError |SystemError ): void {switch (true) {caseerror instanceofUserError :console .log (`User error for ${error .user }: ${error .message }`);break;caseerror instanceofSystemError :console .log (`System error for ${error .system }: ${error .message }`);break;default:console .log ("Unknown error type");}}
Cũng có thể sử dụng hàm type guard do người dùng định nghĩa với switch (true).
tsfunctionhandleValue (value :Panda |Broccoli |User ): void {switch (true) {caseisPanda (value ):console .log (`I am a panda: ${value .panda }`);break;caseisBroccoli (value ):console .log (`I am broccoli: ${value .broccoli }`);break;caseisUser (value ):console .log (`I am ${value .name }`);break;}}
tsfunctionhandleValue (value :Panda |Broccoli |User ): void {switch (true) {caseisPanda (value ):console .log (`I am a panda: ${value .panda }`);break;caseisBroccoli (value ):console .log (`I am broccoli: ${value .broccoli }`);break;caseisUser (value ):console .log (`I am ${value .name }`);break;}}
Thông tin liên quan
📄️ Kiểu any
Kiểu any trong TypeScript là kiểu cho phép gán bất kỳ giá trị nào. Dù là primitive type hay object, bạn có thể gán gì vào cũng không gây lỗi.
📄️ Sự khác biệt giữa any và unknown
Cả hai kiểu any và unknown đều có thể được gán bất kỳ giá trị nào.