Hầu hết mọi đều yêu cầu tuần tự hóa dữ liệu. Nhu cầu này phát sinh trong các tình huống như: ứng dụng web Truyền dữ liệu qua mạng (ví dụ: yêu cầu HTTP, WebSockets) Nhúng dữ liệu vào HTML (ví dụ: để hydrat hóa) Lưu trữ dữ liệu trong bộ lưu trữ liên tục (như LocalStorage) Chia sẻ dữ liệu giữa các quy trình (như nhân viên web hoặc postMessage) Trong nhiều trường hợp, việc mất hoặc hỏng dữ liệu có thể dẫn đến hậu quả nghiêm trọng, do đó việc cung cấp cơ chế tuần tự hóa thuận tiện và an toàn giúp phát hiện nhiều lỗi nhất có thể trong giai đoạn phát triển là điều cần thiết. Vì những mục đích này, sẽ rất thuận tiện khi sử dụng làm định dạng truyền dữ liệu và TypeScript để kiểm tra mã tĩnh trong quá trình phát triển. JSON TypeScript đóng vai trò như một siêu bộ JavaScript, cho phép sử dụng liền mạch các hàm như và , phải không? Hóa ra, bất chấp tất cả những lợi ích của nó, TypeScript không tự nhiên hiểu JSON là gì và loại dữ liệu nào an toàn cho việc tuần tự hóa và giải tuần tự hóa thành JSON. JSON.stringify JSON.parse Hãy minh họa điều này bằng một ví dụ. Sự cố với JSON trong TypeScript Ví dụ, hãy xem xét một hàm lưu một số dữ liệu vào LocalStorage. Vì LocalStorage không thể lưu trữ các đối tượng nên chúng tôi sử dụng tuần tự hóa JSON tại đây: interface PostComment { authorId: string; text: string; updatedAt: Date; } function saveComment(comment: PostComment) { const serializedComment = JSON.stringify(comment); localStorage.setItem('draft', serializedComment); } Chúng tôi cũng sẽ cần một chức năng để lấy dữ liệu từ LocalStorage. function restoreComment(): PostComment | undefined { const text = localStorage.getItem('draft'); return text ? JSON.parse(text) : undefined; } Có gì sai với mã này? Vấn đề đầu tiên là khi khôi phục lại bình luận, chúng ta sẽ nhận được kiểu thay vì cho . string Date updatedAt Điều này xảy ra vì JSON chỉ có bốn kiểu dữ liệu nguyên thủy ( , , , ), cũng như các mảng và đối tượng. Không thể lưu đối tượng trong JSON, cũng như các đối tượng khác có trong JavaScript: hàm, Bản đồ, Tập hợp, v.v. null string number boolean Date Khi gặp một giá trị không thể biểu diễn ở định dạng JSON, việc truyền kiểu sẽ xảy ra. Trong trường hợp đối tượng , chúng ta nhận được một chuỗi vì đối tượng triển khai phương thức , phương thức này trả về một chuỗi thay vì đối tượng . JSON.stringify Date Date toJson() Date const date = new Date('August 19, 1975 23:15:30 UTC'); const jsonDate = date.toJSON(); console.log(jsonDate); // Expected output: "1975-08-19T23:15:30.000Z" const isEqual = date.toJSON() === JSON.stringify(date); console.log(isEqual); // Expected output: true Vấn đề thứ hai là hàm trả về kiểu , trong đó trường ngày thuộc kiểu . Nhưng chúng ta đã biết rằng thay vì , chúng ta sẽ nhận được một loại . TypeScript có thể giúp chúng ta tìm ra lỗi này, nhưng tại sao lại không? saveComment PostComment Date Date string Hóa ra, trong thư viện chuẩn của TypeScript, hàm được gõ là . Do việc sử dụng , việc kiểm tra loại về cơ bản bị vô hiệu hóa. Trong ví dụ của chúng tôi, TypeScript chỉ đơn giản hiểu rằng hàm sẽ trả về một chứa đối tượng . JSON.parse (text: string) => any any PostComment Date Hành vi TypeScript này bất tiện và không an toàn. Ứng dụng của chúng tôi có thể gặp sự cố nếu chúng tôi cố gắng xử lý một chuỗi như đối tượng . Ví dụ: nó có thể bị hỏng nếu chúng ta gọi . Date comment.updatedAt.toLocaleDateString() Thật vậy, trong ví dụ nhỏ của chúng tôi, chúng tôi có thể chỉ cần thay thế đối tượng bằng dấu thời gian bằng số, hoạt động tốt cho việc tuần tự hóa JSON. Tuy nhiên, trong các ứng dụng thực, các đối tượng dữ liệu có thể mở rộng, các loại có thể được xác định ở nhiều vị trí và việc xác định lỗi như vậy trong quá trình phát triển có thể là một nhiệm vụ đầy thách thức. Date Điều gì sẽ xảy ra nếu chúng ta có thể nâng cao hiểu biết của TypeScript về JSON? Xử lý việc tuần tự hóa Để bắt đầu, hãy tìm cách làm cho TypeScript hiểu loại dữ liệu nào có thể được tuần tự hóa thành JSON một cách an toàn. Giả sử chúng ta muốn tạo một hàm , trong đó TypeScript sẽ kiểm tra định dạng dữ liệu đầu vào để đảm bảo nó có thể tuần tự hóa JSON. safeJsonStringify function safeJsonStringify(data: JSONValue) { return JSON.stringify(data); } Trong hàm này, phần quan trọng nhất là loại , đại diện cho tất cả các giá trị có thể được biểu thị ở định dạng JSON. Việc thực hiện khá đơn giản: JSONValue type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; }; Đầu tiên, chúng tôi xác định loại , mô tả tất cả các loại dữ liệu JSON nguyên thủy. Chúng tôi cũng bao gồm loại dựa trên thực tế là khi được tuần tự hóa, các khóa có giá trị sẽ bị bỏ qua. Trong quá trình khử lưu huỳnh, các khóa này sẽ không xuất hiện trong đối tượng, điều này trong hầu hết các trường hợp đều giống nhau. JSONPrimitive undefined undefined Tiếp theo, chúng tôi mô tả loại . Loại này sử dụng khả năng của TypeScript để mô tả các loại đệ quy, là các loại tham chiếu đến chính chúng. Ở đây, có thể là , một mảng hoặc một đối tượng trong đó tất cả các giá trị đều thuộc loại . Kết quả là, một biến thuộc loại này có thể chứa các mảng và đối tượng với khả năng lồng nhau không giới hạn. Các giá trị trong đó cũng sẽ được kiểm tra tính tương thích với định dạng JSON. JSONValue JSONValue JSONPrimitive JSONValue JSONValue JSONValue Bây giờ chúng ta có thể kiểm tra hàm của mình bằng các ví dụ sau: safeJsonStringify // No errors safeJsonStringify({ updatedAt: Date.now() }); // Yields an error: // Argument of type '{ updatedAt: Date; }' is not assignable to parameter of type 'JSONValue'. // Types of property 'updatedAt' are incompatible. // Type 'Date' is not assignable to type 'JSONValue'. safeJsonStringify({ updatedAt: new Date(); }); Mọi thứ dường như hoạt động đúng. Hàm này cho phép chúng ta truyền ngày dưới dạng số nhưng sẽ báo lỗi nếu truyền đối tượng . Date Nhưng hãy xem xét một ví dụ thực tế hơn, trong đó dữ liệu được truyền đến hàm được lưu trữ trong một biến và có kiểu được mô tả. interface PostComment { authorId: string; text: string; updatedAt: number; }; const comment: PostComment = {...}; // Yields an error: // Argument of type 'PostComment' is not assignable to parameter of type 'JSONValue'. // Type 'PostComment' is not assignable to type '{ [key: string]: JSONValue; }'. // Index signature for type 'string' is missing in type 'PostComment'. safeJsonStringify(comment); Bây giờ, mọi thứ đang trở nên phức tạp hơn một chút. TypeScript sẽ không cho phép chúng ta gán biến loại cho tham số hàm loại , vì "Chữ ký chỉ mục cho loại 'chuỗi' bị thiếu trong loại 'PostComment'". PostComment JSONValue Vậy chữ ký chỉ mục là gì và tại sao nó lại bị thiếu? Hãy nhớ cách chúng tôi mô tả các đối tượng có thể được tuần tự hóa sang định dạng JSON? type JSONValue = { [key: string]: JSONValue; }; Trong trường hợp này, là chữ ký chỉ mục. Nó có nghĩa là "đối tượng này có thể có bất kỳ khóa nào ở dạng chuỗi, các giá trị của chúng có loại ". Vì vậy, hóa ra chúng ta cần thêm chữ ký chỉ mục vào loại , phải không? [key: string] JSONValue PostComment interface PostComment { authorId: string; text: string; updatedAt: number; // Don't do this: [key: string]: JSONValue; }; Làm như vậy có nghĩa là nhận xét có thể chứa bất kỳ trường tùy ý nào, đây thường không phải là kết quả mong muốn khi xác định loại dữ liệu trong ứng dụng. Giải pháp thực sự cho vấn đề với chữ ký chỉ mục đến từ , cho phép lặp lại đệ quy trên các trường, ngay cả đối với các loại không có chữ ký chỉ mục được xác định. Kết hợp với generics, tính năng này cho phép chuyển đổi bất kỳ loại dữ liệu nào thành loại khác , tương thích với định dạng JSON. Các loại đã ánh xạ T JSONCompatible<T> type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; type NotAssignableToJson = | bigint | symbol | Function; Loại là loại được ánh xạ để kiểm tra xem loại đã cho có thể được tuần tự hóa thành JSON một cách an toàn hay không. Nó thực hiện điều này bằng cách lặp lại từng thuộc tính trong loại và thực hiện như sau: JSONCompatible<T> T T loại có điều kiện xác minh xem loại thuộc tính có tương thích với loại hay không, đảm bảo nó có thể được chuyển đổi thành JSON một cách an toàn. Trong trường hợp này, loại thuộc tính vẫn không thay đổi. T[P] extends JSONValue ? T[P] : ... JSONValue loại có điều kiện xác minh xem loại thuộc tính có thể được gán cho JSON hay không. Trong trường hợp này, loại thuộc tính được chuyển đổi thành , lọc thuộc tính ra khỏi loại cuối cùng một cách hiệu quả. T[P] extends NotAssignableToJson ? never : ... never Nếu cả hai điều kiện này đều không được đáp ứng, loại sẽ được kiểm tra đệ quy cho đến khi có thể đưa ra kết luận. Bằng cách này, nó hoạt động ngay cả khi loại không có chữ ký chỉ mục. Cái kiểm tra lúc đầu được sử dụng để ngăn loại bị chuyển đổi thành loại đối tượng trống , về cơ bản tương đương với loại . unknown extends T ? never :... unknown {} any Một khía cạnh thú vị khác là loại . Nó bao gồm hai nguyên hàm TypeScript (bigint và ký hiệu) và loại , mô tả bất kỳ hàm nào có thể. Loại rất quan trọng trong việc lọc ra bất kỳ giá trị nào không thể gán cho JSON. Điều này là do bất kỳ đối tượng phức tạp nào trong JavaScript đều dựa trên loại Đối tượng và có ít nhất một hàm trong chuỗi nguyên mẫu của nó (ví dụ: ). Loại lặp lại tất cả các hàm đó, vì vậy, việc kiểm tra các hàm là đủ để lọc ra bất kỳ thứ gì không thể tuần tự hóa thành JSON. NotAssignableToJson Function Function toString() JSONCompatible Bây giờ, hãy sử dụng loại này trong hàm tuần tự hóa: function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); } Bây giờ, hàm sử dụng tham số chung và chấp nhận đối số . Điều này có nghĩa là nó lấy đối số thuộc loại , loại này phải là loại tương thích với JSON. Bây giờ chúng ta có thể sử dụng hàm với các kiểu dữ liệu mà không cần chữ ký chỉ mục. T JSONCompatible<T> data T Hàm hiện sử dụng tham số chung mở rộng từ loại . Điều này có nghĩa là nó chấp nhận đối số thuộc loại , phải là loại tương thích với JSON. Kết quả là chúng ta có thể sử dụng hàm với các kiểu dữ liệu thiếu chữ ký chỉ mục. T JSONCompatible<T> data T interface PostComment { authorId: string; text: string; updatedAt: number; } function saveComment(comment: PostComment) { const serializedComment = safeJsonStringify(comment); localStorage.setItem('draft', serializedComment); } Cách tiếp cận này có thể được sử dụng bất cứ khi nào cần tuần tự hóa JSON, chẳng hạn như truyền dữ liệu qua mạng, nhúng dữ liệu vào HTML, lưu trữ dữ liệu trong localStorage, truyền dữ liệu giữa các nhân viên, v.v. Ngoài ra, trình trợ giúp có thể được sử dụng khi một đối tượng được nhập đúng không có chữ ký chỉ mục cần được gán cho một biến kiểu . toJsonValue JSONValue function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } const comment: PostComment = {...}; const data: JSONValue = { comment: toJsonValue(comment) }; Trong ví dụ này, việc sử dụng cho phép chúng tôi bỏ qua lỗi liên quan đến chữ ký chỉ mục bị thiếu trong loại . toJsonValue PostComment Xử lý quá trình khử lưu huỳnh Khi nói đến quá trình khử lưu huỳnh, thách thức vừa đơn giản vừa phức tạp hơn vì nó bao gồm cả kiểm tra phân tích tĩnh và kiểm tra thời gian chạy đối với định dạng của dữ liệu nhận được. Từ góc nhìn của hệ thống kiểu chữ của TypeScript, thử thách này khá đơn giản. Hãy xem xét ví dụ sau: function safeJsonParse(text: string) { return JSON.parse(text) as unknown; } const data = JSON.parse(text); // ^? unknown Trong trường hợp này, chúng tôi thay thế kiểu trả về bằng kiểu . Tại sao chọn ? Về cơ bản, một chuỗi JSON có thể chứa bất kỳ thứ gì, không chỉ dữ liệu mà chúng ta mong đợi nhận được. Ví dụ: định dạng dữ liệu có thể thay đổi giữa các phiên bản ứng dụng khác nhau hoặc một phần khác của ứng dụng có thể ghi dữ liệu vào cùng một khóa LocalStorage. Vì vậy, là sự lựa chọn an toàn và chính xác nhất. any unknown unknown unknown Tuy nhiên, làm việc với kiểu dữ liệu sẽ kém thuận tiện hơn so với việc chỉ xác định kiểu dữ liệu mong muốn. Ngoài việc ép kiểu, còn có nhiều cách để chuyển đổi kiểu thành kiểu dữ liệu được yêu cầu. Một phương pháp như vậy là sử dụng thư viện để xác thực dữ liệu trong thời gian chạy và đưa ra các lỗi chi tiết nếu dữ liệu không hợp lệ. unknown unknown Superstruct import { create, object, number, string } from 'superstruct'; const PostComment = object({ authorId: string(), text: string(), updatedAt: number(), }); // Note: we no longer need to manually specify the return type function restoreDraft() { const text = localStorage.getItem('draft'); return text ? create(JSON.parse(text), PostComment) : undefined; } Ở đây, hàm hoạt động như một trình bảo vệ kiểu, loại vào giao diện mong muốn. Do đó, chúng ta không cần phải chỉ định kiểu trả về theo cách thủ công nữa. create thu hẹp Comment Việc triển khai tùy chọn khử lưu huỳnh an toàn mới chỉ là một nửa câu chuyện. Điều quan trọng không kém là đừng quên sử dụng nó khi giải quyết nhiệm vụ tiếp theo trong dự án. Điều này trở nên đặc biệt khó khăn nếu một nhóm lớn đang thực hiện dự án, vì việc đảm bảo tuân thủ tất cả các thỏa thuận và thực tiễn tốt nhất có thể khó khăn. có thể hỗ trợ công việc này. Công cụ này giúp xác định tất cả các trường hợp sử dụng an toàn. Cụ thể, tất cả các cách sử dụng đều có thể được tìm thấy và có thể đảm bảo rằng định dạng của dữ liệu nhận được đã được kiểm tra. Bạn có thể đọc thêm về cách loại bỏ loại nào trong cơ sở mã trong bài viết . Typescript-eslint any JSON.parse any Tạo TypeScript thực sự "được gõ mạnh" Phần kết luận Dưới đây là các hàm và kiểu tiện ích cuối cùng được thiết kế để hỗ trợ quá trình tuần tự hóa và giải tuần tự hóa JSON an toàn. Bạn có thể kiểm tra những điều này trong đã được chuẩn bị sẵn. TS Playground type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONValue[] | { [key: string]: JSONValue; }; type NotAssignableToJson = | bigint | symbol | Function; type JSONCompatible<T> = unknown extends T ? never : { [P in keyof T]: T[P] extends JSONValue ? T[P] : T[P] extends NotAssignableToJson ? never : JSONCompatible<T[P]>; }; function toJsonValue<T>(value: JSONCompatible<T>): JSONValue { return value; } function safeJsonStringify<T>(data: JSONCompatible<T>) { return JSON.stringify(data); } function safeJsonParse(text: string): unknown { return JSON.parse(text); } Chúng có thể được sử dụng trong mọi tình huống cần tuần tự hóa JSON. Tôi đã sử dụng chiến lược này trong các dự án của mình được vài năm và nó đã chứng minh tính hiệu quả của nó bằng cách phát hiện kịp thời các lỗi tiềm ẩn trong quá trình phát triển ứng dụng. Tôi hy vọng bài viết này đã cung cấp cho bạn một số hiểu biết mới. Cảm ơn bạn đã đọc! Liên kết hữu ích Làm cho TypeScript thực sự được "gõ mạnh" Cải thiện các loại thư viện tiêu chuẩn của TypeScript Thư viện siêu phẩm sân chơi TS