Discriminated union (union có thể phân biệt)

Discriminated union của TypeScript là union type đặc biệt, có "dấu hiệu" để phân biệt từng kiểu object trong union. Khi thu hẹp union type gồm các kiểu object mà logic phân nhánh phức tạp, sử dụng discriminated union sẽ cải thiện khả năng đọc và bảo trì code.
Union type thông thường có việc thu hẹp phức tạp
Union type của TypeScript có độ tự do cao, cho phép kết hợp bất kỳ kiểu nào. UploadStatus sau đây là union type biểu diễn trạng thái upload file. Là sự kết hợp của đang upload InProgress, upload thành công Success, upload thất bại Failure.
tstypeUploadStatus =InProgress |Success |Failure ;typeInProgress = {done : boolean;progress : number };typeSuccess = {done : boolean };typeFailure = {done : boolean;error :Error };
tstypeUploadStatus =InProgress |Success |Failure ;typeInProgress = {done : boolean;progress : number };typeSuccess = {done : boolean };typeFailure = {done : boolean;error :Error };
Bảng sau tổng hợp các trạng thái của UploadStatus.
| Kiểu | Ý nghĩa | done | progress | error |
|---|---|---|---|---|
InProgress | Đang upload | false | Tiến độ(%) | - |
Success | Upload thành công | true | - | - |
Failure | Upload thất bại | true | - | Chi tiết lỗi |
Hãy thử implement function hiển thị trạng thái.
tsfunctionprintStatus (status :UploadStatus ) {if (status .done === false) {Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.2339Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.console .log (`Đang upload: ${status .}%`); progress }}
tsfunctionprintStatus (status :UploadStatus ) {if (status .done === false) {Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.2339Property 'progress' does not exist on type 'UploadStatus'. Property 'progress' does not exist on type 'Success'.console .log (`Đang upload: ${status .}%`); progress }}
Implementation này kiểm tra done là false. Không có bug. Tuy nhiên, compiler cảnh báo không có progress. Vì ngay cả trong nhánh if, compiler vẫn nghĩ status có thể là Success hoặc Failure.
Để giải quyết lỗi này, cần kiểm tra có progress hay không. Như vậy, compiler sẽ xác định status trong nhánh if là InProgress.
tsfunctionprintStatus (status :UploadStatus ) {if (status .done === false && "progress" instatus ) {// ^^^^^^^^^^^^^^^^^^^^thêmconsole .log (`Đang upload: ${status .progress }%`);// Lỗi compile được giải quyết!}}
tsfunctionprintStatus (status :UploadStatus ) {if (status .done === false && "progress" instatus ) {// ^^^^^^^^^^^^^^^^^^^^thêmconsole .log (`Đang upload: ${status .progress }%`);// Lỗi compile được giải quyết!}}
Function sau xử lý tất cả trạng thái mà không gây lỗi compile.
tsfunctionprintStatus (status :UploadStatus ) {if (status .done ) {if ("error" instatus ) {console .log (`Upload thất bại: ${status .error .message }`);} else {console .log ("Upload thành công");}} else if ("progress" instatus ) {console .log (`Đang upload: ${status .progress }%`);}}
tsfunctionprintStatus (status :UploadStatus ) {if (status .done ) {if ("error" instatus ) {console .log (`Upload thất bại: ${status .error .message }`);} else {console .log ("Upload thành công");}} else if ("progress" instatus ) {console .log (`Đang upload: ${status .progress }%`);}}
Code này có vẻ lộn xộn và không dễ đọc. Với union type của object như vậy, nên viết lại thành discriminated union. Sẽ dễ đọc và bảo trì hơn.
Discriminated union là gì?
Discriminated union của TypeScript là ứng dụng của union type. Discriminated union còn được gọi là tagged union hoặc sum type.
Discriminated union là union type có các đặc điểm sau.
- Union type gồm các kiểu object
- Có property (dấu hiệu) để phân biệt từng kiểu object
- Property này được gọi là discriminator
- Kiểu của discriminator phải là literal type, v.v.
- Miễn có discriminator, mỗi kiểu object có thể có property riêng
Ví dụ, viết lại UploadStatus trên thành discriminated union như sau.
tstypeUploadStatus =InProgress |Success |Failure ;typeInProgress = {type : "InProgress";progress : number };typeSuccess = {type : "Success" };typeFailure = {type : "Failure";error :Error };
tstypeUploadStatus =InProgress |Success |Failure ;typeInProgress = {type : "InProgress";progress : number };typeSuccess = {type : "Success" };typeFailure = {type : "Failure";error :Error };
Bảng tổng hợp như sau.
| Kiểu | Ý nghĩa | Discriminator | progress | error |
|---|---|---|---|---|
InProgress | Đang upload | type: "InProgress" | Tiến độ(%) | - |
Success | Upload thành công | type: "Success" | - | - |
Failure | Upload thất bại | type: "Failure" | - | Chi tiết lỗi |
Điểm thay đổi là done: boolean đã biến mất, và discriminator type được thêm vào. Điểm quan trọng nữa là kiểu của type không phải string mà là literal type như InProgress.
Thu hẹp discriminated union
Discriminated union được thu hẹp kiểu khi phân nhánh discriminator.
tsfunctionprintStatus (status :UploadStatus ) {if (status .type === "InProgress") {console .log (`Uploading: ${status .progress }%`);} else if (status .type === "Success") {console .log ("Upload success",status );} else if (status .type === "Failure") {console .log (`Upload failed: ${status .error .message }`);} else {console .log ("Invalid status: ",status );}}
tsfunctionprintStatus (status :UploadStatus ) {if (status .type === "InProgress") {console .log (`Uploading: ${status .progress }%`);} else if (status .type === "Success") {console .log ("Upload success",status );} else if (status .type === "Failure") {console .log (`Upload failed: ${status .error .message }`);} else {console .log ("Invalid status: ",status );}}
Viết bằng switch cũng được compiler hiểu việc thu hẹp tương tự.
tsfunctionprintStatus (status :UploadStatus ) {switch (status .type ) {case "InProgress":console .log (`Đang upload: ${status .progress }%`);break;case "Success":console .log ("Upload thành công",status );break;case "Failure":console .log (`Upload thất bại: ${status .error .message }`);break;default:console .log ("Trạng thái không hợp lệ: ",status );}}
tsfunctionprintStatus (status :UploadStatus ) {switch (status .type ) {case "InProgress":console .log (`Đang upload: ${status .progress }%`);break;case "Success":console .log ("Upload thành công",status );break;case "Failure":console .log (`Upload thất bại: ${status .error .message }`);break;default:console .log ("Trạng thái không hợp lệ: ",status );}}
Sử dụng discriminated union giúp compiler hiểu được việc thu hẹp kiểu. Kết quả là xử lý phân nhánh dễ đọc và bảo trì hơn.
Kiểu có thể dùng cho discriminator
Các kiểu có thể dùng cho discriminator là literal type, null, và undefined.
- Literal type
- String literal type: (ví dụ)
"success","OK", v.v. - Number literal type: (ví dụ)
1,200, v.v. - Boolean literal type:
truehoặcfalse
- String literal type: (ví dụ)
nullundefined
Trong UploadStatus trên, chúng ta dùng string literal type làm discriminator. Literal type còn có number và boolean. Chúng cũng có thể dùng làm discriminator.
Discriminator là number literal typetstypeOkOrBadRequest =| {statusCode : 200;value : string }| {statusCode : 400;message : string };functionhandleResponse (x :OkOrBadRequest ) {if (x .statusCode === 200) {console .log (x .value );} else {console .log (x .message );}}
Discriminator là number literal typetstypeOkOrBadRequest =| {statusCode : 200;value : string }| {statusCode : 400;message : string };functionhandleResponse (x :OkOrBadRequest ) {if (x .statusCode === 200) {console .log (x .value );} else {console .log (x .message );}}
Discriminator là boolean literal typetstypeOkOrNotOk =| {isOK : true;value : string }| {isOK : false;error : string };functionhandleStatus (x :OkOrNotOk ) {if (x .isOK ) {console .log (x .value );} else {console .log (x .error );}}
Discriminator là boolean literal typetstypeOkOrNotOk =| {isOK : true;value : string }| {isOK : false;error : string };functionhandleStatus (x :OkOrNotOk ) {if (x .isOK ) {console .log (x .value );} else {console .log (x .error );}}
Kiểu có quan hệ null và non-null cũng có thể làm discriminator. Trong ví dụ sau, property error là null hoặc Error, tạo thành quan hệ null/non-null.
tstypeResult =| {error : null;value : string }| {error :Error };functionhandleResult (result :Result ) {if (result .error === null) {console .log (result .value );} else {console .log (result .error );}}
tstypeResult =| {error : null;value : string }| {error :Error };functionhandleResult (result :Result ) {if (result .error === null) {console .log (result .value );} else {console .log (result .error );}}
Tương tự, undefined cũng có thể làm discriminator với property có quan hệ undefined/non-undefined.
tstypeResult =| {error : undefined;value : string }| {error :Error };functionhandleResult (result :Result ) {if (result .error ) {console .log (result .error );} else {console .log (result .value );}}
tstypeResult =| {error : undefined;value : string }| {error :Error };functionhandleResult (result :Result ) {if (result .error ) {console .log (result .error );} else {console .log (result .value );}}
Khi gán discriminator vào biến
Khi gán discriminator vào biến và dùng biến đó trong điều kiện phân nhánh, vẫn có thể thu hẹp kiểu.
tstypeShape =| {type : "circle";color : string;radius : number }| {type : "square";color : string;size : number };functiontoCSS (shape :Shape ) {const {type ,color } =shape ;// ^^^^discriminatorswitch (type ) {case "circle":return {background :color ,borderRadius :shape .radius ,};case "square":return {background :color ,width :shape .size ,height :shape .size ,};}}
tstypeShape =| {type : "circle";color : string;radius : number }| {type : "square";color : string;size : number };functiontoCSS (shape :Shape ) {const {type ,color } =shape ;// ^^^^discriminatorswitch (type ) {case "circle":return {background :color ,borderRadius :shape .radius ,};case "square":return {background :color ,width :shape .size ,height :shape .size ,};}}
Chia sẻ kiến thức
Discriminated union của TypeScript
・Union type gồm các kiểu object có discriminator
・Dễ thu hẹp kiểu với if/switch
Discriminator
・Property key chung của các object (như dấu hiệu)
・Kiểu có thể dùng: literal type, null, undefined
Từ 『Survival TypeScript』
Thông tin liên quan
📄️ Union type
Union type trong TypeScript là cách biểu diễn "một trong các kiểu".