paint-brush
Làm cho TypeScript thực sự được "gõ mạnh"từ tác giả@nodge
17,366 lượt đọc
17,366 lượt đọc

Làm cho TypeScript thực sự được "gõ mạnh"

từ tác giả Maksim Zemskov12m2023/09/10
Read on Terminal Reader

dài quá đọc không nổi

TypeScript cung cấp loại "Bất kỳ" cho các trường hợp không biết trước hình dạng của dữ liệu. Tuy nhiên, việc sử dụng quá nhiều loại này có thể dẫn đến các vấn đề về an toàn loại, chất lượng mã và trải nghiệm của nhà phát triển. Bài viết này khám phá những rủi ro liên quan đến loại "Bất kỳ", xác định các nguồn tiềm năng để đưa nó vào cơ sở mã và cung cấp các chiến lược để kiểm soát việc sử dụng nó trong suốt dự án.
featured image - Làm cho TypeScript thực sự được "gõ mạnh"
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript tuyên bố là ngôn ngữ lập trình được gõ mạnh mẽ được xây dựng dựa trên JavaScript, cung cấp công cụ tốt hơn ở mọi quy mô. Tuy nhiên, TypeScript bao gồm any loại nào, thường có thể lẻn vào cơ sở mã một cách ngầm định và dẫn đến mất đi nhiều ưu điểm của TypeScript.


Bài viết này khám phá các cách để kiểm soát any loại nào trong các dự án TypeScript. Hãy sẵn sàng giải phóng sức mạnh của TypeScript, đạt được độ an toàn cao nhất về kiểu chữ và cải thiện chất lượng mã.

Nhược điểm của việc sử dụng Any trong TypeScript

TypeScript cung cấp một loạt công cụ bổ sung để nâng cao trải nghiệm và năng suất của nhà phát triển:


  • Nó giúp bắt lỗi sớm trong giai đoạn phát triển.
  • Nó cung cấp khả năng tự động hoàn thành tuyệt vời cho các trình soạn thảo mã và IDE.
  • Nó cho phép tái cấu trúc dễ dàng các cơ sở mã lớn thông qua các công cụ điều hướng mã tuyệt vời và tái cấu trúc tự động.
  • Nó đơn giản hóa sự hiểu biết về cơ sở mã bằng cách cung cấp ngữ nghĩa bổ sung và cấu trúc dữ liệu rõ ràng thông qua các loại.


Tuy nhiên, ngay khi bạn bắt đầu sử dụng any loại nào trong cơ sở mã của mình, bạn sẽ mất tất cả các lợi ích được liệt kê ở trên. Kiểu any là một lỗ hổng nguy hiểm trong hệ thống kiểu và việc sử dụng nó sẽ vô hiệu hóa tất cả các khả năng kiểm tra kiểu cũng như tất cả các công cụ phụ thuộc vào việc kiểm tra kiểu. Kết quả là, tất cả lợi ích của TypeScript đều bị mất: lỗi bị bỏ qua, trình soạn thảo mã trở nên kém hữu ích hơn và hơn thế nữa.


Ví dụ, hãy xem xét ví dụ sau:


 function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any


Trong đoạn mã trên:


  • Bạn sẽ bỏ lỡ tính năng tự động hoàn thành bên trong hàm parse . Khi bạn gõ data. trong trình chỉnh sửa của mình, bạn sẽ không nhận được đề xuất chính xác về các phương pháp có sẵn cho data .
  • Trong trường hợp đầu tiên, có lỗi TypeError: data.split is not a function vì chúng tôi đã chuyển một số thay vì một chuỗi. TypeScript không thể đánh dấu lỗi vì any tác đều vô hiệu hóa việc kiểm tra loại.
  • Trong trường hợp thứ hai, biến res2 cũng có kiểu any . Điều này có nghĩa là một lần sử dụng any cũng có thể có tác động xếp tầng trên một phần lớn cơ sở mã.


Sử dụng any chỉ được chấp nhận trong những trường hợp đặc biệt hoặc cho nhu cầu tạo mẫu. Nói chung, tốt hơn hết là tránh sử dụng any để tận dụng tối đa TypeScript.

Bất kỳ loại nào đến từ đâu

Điều quan trọng là phải biết nguồn của any loại nào trong cơ sở mã vì việc viết rõ ràng any không phải là lựa chọn duy nhất. Bất chấp những nỗ lực tốt nhất của chúng tôi để tránh sử dụng any loại nào, đôi khi nó có thể ngấm ngầm xâm nhập vào cơ sở mã.


Có bốn nguồn chính thuộc loại any trong cơ sở mã:

  1. Tùy chọn trình biên dịch trong tsconfig.
  2. Thư viện chuẩn của TypeScript.
  3. Sự phụ thuộc của dự án.
  4. Sử dụng rõ ràng any trong một cơ sở mã.


Tôi đã viết các bài về Những điều cần cân nhắc chính trong tsconfigCải thiện các loại thư viện tiêu chuẩn cho hai điểm đầu tiên. Vui lòng kiểm tra chúng nếu bạn muốn cải thiện tính an toàn trong dự án của mình.


Lần này, chúng ta sẽ tập trung vào các công cụ tự động để kiểm soát sự xuất hiện của any loại nào trong cơ sở mã.

Giai đoạn 1: Sử dụng ESLint

ESLint là một công cụ phân tích tĩnh phổ biến được các nhà phát triển web sử dụng để đảm bảo các phương pháp thực hành tốt nhất và định dạng mã. Nó có thể được sử dụng để thực thi các kiểu mã hóa và tìm mã không tuân thủ các nguyên tắc nhất định.


ESLint cũng có thể được sử dụng với các dự án TypeScript nhờ plugin typectipt-eslint . Rất có thể plugin này đã được cài đặt trong dự án của bạn. Nhưng nếu không, bạn có thể làm theo hướng dẫn bắt đầu chính thức.


Cấu hình phổ biến nhất cho typescript-eslint như sau:


 module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };


Cấu hình này cho phép eslint hiểu TypeScript ở cấp độ cú pháp, cho phép bạn viết các quy tắc eslint đơn giản áp dụng cho các kiểu được viết thủ công trong mã. Ví dụ: bạn có thể cấm sử dụng rõ ràng any .


Giá trị đặt trước recommended chứa một bộ quy tắc ESLint được lựa chọn cẩn thận nhằm cải thiện tính chính xác của mã. Mặc dù bạn nên sử dụng toàn bộ cài đặt trước nhưng vì mục đích của bài viết này, chúng tôi sẽ chỉ tập trung vào quy tắc no-explicit-any .

không rõ ràng-bất kỳ

Chế độ nghiêm ngặt của TypeScript ngăn chặn việc sử dụng ngụ ý any , nhưng nó không ngăn cản any việc sử dụng rõ ràng nào. Quy tắc no-explicit-any giúp cấm viết thủ công any vị trí nào trong cơ sở mã.


 // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}


Mục đích chính của quy tắc này là ngăn chặn việc sử dụng any trong toàn nhóm. Đây là một phương tiện để củng cố sự đồng thuận của nhóm rằng việc sử dụng any trong dự án đều không được khuyến khích.


Đây là một mục tiêu quan trọng vì ngay cả một lần sử dụng any cũng có thể có tác động xếp tầng lên một phần đáng kể của cơ sở mã do suy luận kiểu . Tuy nhiên, điều này vẫn còn lâu mới đạt được mức độ an toàn cuối cùng.

Tại sao không rõ ràng-bất kỳ là không đủ

Mặc dù chúng ta đã giải quyết vấn đề sử dụng rõ ràng any , nhưng vẫn còn nhiều hàm bất kỳ ngụ ý any trong các phần phụ thuộc của dự án, bao gồm các gói npm và thư viện chuẩn của TypeScript.


Hãy xem xét đoạn mã sau, có thể thấy trong bất kỳ dự án nào:


 const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any


Cả hai biến pokemonssettings đều được ngầm định thuộc loại any . Cả chế độ nghiêm ngặt của no-explicit-any và TypeScript đều không cảnh báo chúng tôi trong trường hợp này. Chưa.


Điều này xảy ra vì các kiểu của response.json()JSON.parse() đến từ thư viện chuẩn của TypeScript, nơi các phương thức này có any thích rõ ràng. Chúng tôi vẫn có thể chỉ định thủ công loại tốt hơn cho các biến của mình, nhưng có gần 1.200 lần xuất hiện của any trong thư viện chuẩn. Gần như không thể nhớ được tất cả các trường hợp mà any có thể lẻn vào cơ sở mã của chúng tôi từ thư viện tiêu chuẩn.


Điều tương tự cũng xảy ra với các phụ thuộc bên ngoài. Có nhiều thư viện được gõ kém trong npm, hầu hết vẫn được viết bằng JavaScript. Kết quả là, việc sử dụng các thư viện như vậy có thể dễ dàng dẫn đến nhiều ẩn any trong cơ sở mã.


Nói chung vẫn có nhiều cách để any có thể lẻn vào mã của chúng ta.

Giai đoạn 2: Tăng cường khả năng kiểm tra loại

Lý tưởng nhất là chúng ta muốn có một cài đặt trong TypeScript khiến trình biên dịch phàn nàn về bất kỳ biến nào đã nhận được kiểu any vì bất kỳ lý do gì. Thật không may, cài đặt như vậy hiện không tồn tại và dự kiến sẽ không được thêm vào.


Chúng ta có thể đạt được hành vi này bằng cách sử dụng chế độ kiểm tra kiểu của plugin typescript-eslint . Chế độ này hoạt động cùng với TypeScript để cung cấp thông tin loại hoàn chỉnh từ trình biên dịch TypeScript đến các quy tắc ESLint. Với thông tin này, có thể viết các quy tắc ESLint phức tạp hơn về cơ bản mở rộng khả năng kiểm tra kiểu của TypeScript. Ví dụ: một quy tắc có thể tìm thấy tất cả các biến có kiểu any , any thu được bằng cách nào.


Để sử dụng quy tắc nhận biết loại, bạn cần điều chỉnh một chút cấu hình ESLint:


 module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };


Để bật suy luận kiểu cho typescript-eslint , hãy thêm parserOptions vào cấu hình ESLint. Sau đó, thay thế giá trị đặt recommended bằng recommended-type-checked . Cài đặt trước sau bổ sung thêm khoảng 17 quy tắc mạnh mẽ mới. Với mục đích của bài viết này, chúng tôi sẽ chỉ tập trung vào 5 trong số đó.

đối số không an toàn

Quy tắc no-unsafe-argument tìm kiếm các lệnh gọi hàm trong đó một biến thuộc loại any được truyền dưới dạng tham số. Khi điều này xảy ra, việc kiểm tra kiểu sẽ bị mất và tất cả lợi ích của việc gõ mạnh cũng bị mất.


Ví dụ: hãy xem xét hàm saveForm yêu cầu một đối tượng làm tham số. Giả sử chúng ta nhận được JSON, phân tích cú pháp và thu được any loại nào.


 // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.


Khi chúng ta gọi hàm saveForm với tham số này, quy tắc no-unsafe-argument sẽ đánh dấu nó là không an toàn và yêu cầu chúng ta chỉ định loại thích hợp cho biến value .


Quy tắc này đủ mạnh để kiểm tra sâu các cấu trúc dữ liệu lồng nhau trong các đối số của hàm. Do đó, bạn có thể tin tưởng rằng việc truyền đối tượng dưới dạng đối số hàm sẽ không bao giờ chứa dữ liệu chưa được gõ.


 // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });


Cách tốt nhất để khắc phục lỗi là sử dụng thư viện thu hẹp kiểu của TypeScript hoặc thư viện xác thực như Zod hoặc Superstruct . Ví dụ: hãy viết hàm parseFormValues để thu hẹp loại dữ liệu được phân tích cú pháp chính xác.


 // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);


Lưu ý rằng được phép chuyển any loại nào làm đối số cho hàm chấp nhận unknown vì không có mối lo ngại về an toàn nào liên quan đến việc làm như vậy.


Viết các hàm xác thực dữ liệu có thể là một công việc tẻ nhạt, đặc biệt khi xử lý lượng lớn dữ liệu. Vì vậy, đáng để xem xét việc sử dụng thư viện xác thực dữ liệu. Ví dụ: với Zod, mã sẽ trông như thế này:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);


nhiệm vụ không an toàn

Quy tắc no-unsafe-assignment tìm kiếm các phép gán biến trong đó một giá trị có kiểu any . Những phép gán như vậy có thể khiến trình biên dịch hiểu lầm rằng một biến có một kiểu nhất định, trong khi dữ liệu thực sự có thể có một kiểu khác.


Hãy xem xét ví dụ trước về phân tích cú pháp JSON:


 // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value


Nhờ quy tắc no-unsafe-assignment , chúng ta có thể nắm bắt được any loại nào ngay cả trước khi chuyển formValues đi nơi khác. Chiến lược sửa lỗi vẫn giữ nguyên: Chúng ta có thể sử dụng việc thu hẹp kiểu để cung cấp một kiểu cụ thể cho giá trị của biến.


 // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues


không có quyền truy cập thành viên không an toàn và không có cuộc gọi không an toàn

Hai quy tắc này kích hoạt ít thường xuyên hơn. Tuy nhiên, dựa trên kinh nghiệm của tôi, chúng thực sự hữu ích khi bạn đang cố gắng sử dụng các phần phụ thuộc của bên thứ ba được đánh máy kém.


Quy tắc no-unsafe-member-access sẽ ngăn chúng ta truy cập vào các thuộc tính đối tượng nếu một biến có any loại nào, vì nó có thể là null hoặc undefined .


Quy tắc no-unsafe-call ngăn chúng ta gọi một biến có kiểu any dưới dạng hàm, vì nó có thể không phải là hàm.


Hãy tưởng tượng rằng chúng ta có một thư viện bên thứ ba được đánh máy kém có tên là untyped-auth :


 // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.


Kẻ nói dối nêu bật hai vấn đề:

  • Việc gọi hàm authenticate có thể không an toàn vì chúng ta có thể quên truyền các đối số quan trọng cho hàm.
  • Việc đọc thuộc tính name từ đối tượng userInfo là không an toàn vì nó sẽ null nếu xác thực không thành công.


Cách tốt nhất để khắc phục những lỗi này là cân nhắc sử dụng thư viện có API được định kiểu mạnh. Nhưng nếu đây không phải là một tùy chọn, bạn có thể tự mìnhtăng thêm các loại thư viện . Một ví dụ với các loại thư viện cố định sẽ như thế này:


 // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }


không-không an toàn-trở lại

Quy tắc no-unsafe-return giúp không vô tình trả về any loại nào từ một hàm sẽ trả về một cái gì đó cụ thể hơn. Những trường hợp như vậy có thể khiến trình biên dịch hiểu lầm rằng giá trị được trả về có một loại nhất định, trong khi dữ liệu thực sự có thể có một loại khác.


Ví dụ: giả sử chúng ta có một hàm phân tích JSON và trả về một đối tượng có hai thuộc tính.


 // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null


parseForm cú pháp có thể dẫn đến lỗi thời gian chạy ở bất kỳ phần nào của chương trình mà nó được sử dụng vì giá trị được phân tích cú pháp không được kiểm tra. Quy tắc no-unsafe-return sẽ ngăn chặn các vấn đề thời gian chạy như vậy.


Việc khắc phục điều này thật dễ dàng bằng cách thêm xác thực để đảm bảo rằng JSON được phân tích cú pháp khớp với loại dự kiến. Hãy sử dụng thư viện Zod lần này:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }


Lưu ý về hiệu suất

Việc sử dụng các quy tắc được kiểm tra kiểu sẽ đi kèm với một hình phạt về hiệu suất đối với ESLint vì nó phải gọi trình biên dịch của TypeScript để suy ra tất cả các loại. Sự chậm lại này chủ yếu dễ nhận thấy khi chạy kẻ nói dối trong pre-commit hook và trong CI, nhưng không đáng chú ý khi làm việc trong IDE. Việc kiểm tra loại được thực hiện một lần khi khởi động IDE và sau đó cập nhật các loại khi bạn thay đổi mã.


Điều đáng lưu ý là chỉ suy ra các kiểu sẽ hoạt động nhanh hơn cách gọi thông thường của trình biên dịch tsc . Ví dụ: trong dự án gần đây nhất của chúng tôi với khoảng 1,5 triệu dòng mã TypeScript, việc kiểm tra kiểu thông qua tsc mất khoảng 11 phút, trong khi thời gian bổ sung cần thiết để các quy tắc nhận biết kiểu của ESLint khởi động chỉ là khoảng 2 phút.


Đối với nhóm của chúng tôi, sự an toàn bổ sung được cung cấp bằng cách sử dụng các quy tắc phân tích tĩnh nhận biết loại là đáng để đánh đổi. Đối với các dự án nhỏ hơn, quyết định này thậm chí còn dễ thực hiện hơn.

Phần kết luận

Kiểm soát việc sử dụng any dự án TypeScript nào là rất quan trọng để đạt được chất lượng mã và an toàn loại tối ưu. Bằng cách sử dụng plugin typescript-eslint , các nhà phát triển có thể xác định và loại bỏ mọi sự xuất hiện của any loại nào trong cơ sở mã của họ, tạo ra một cơ sở mã mạnh mẽ hơn và dễ bảo trì hơn.


Bằng cách sử dụng các quy tắc eslint nhận biết loại, bất kỳ sự xuất hiện nào của từ khóa any trong cơ sở mã của chúng tôi sẽ là một quyết định có chủ ý chứ không phải là một sai lầm hoặc sơ suất. Cách tiếp cận này bảo vệ chúng tôi khỏi việc sử dụng any mã nào trong mã của riêng chúng tôi, cũng như trong thư viện tiêu chuẩn và các phần phụ thuộc của bên thứ ba.


Nhìn chung, kẻ nói dối nhận biết kiểu cho phép chúng ta đạt được mức độ an toàn về kiểu tương tự như mức độ của các ngôn ngữ lập trình kiểu tĩnh như Java, Go, Rust và các ngôn ngữ khác. Điều này giúp đơn giản hóa rất nhiều việc phát triển và bảo trì các dự án lớn.


Tôi hy vọng bạn đã học được điều gì đó mới từ bài viết này. Cảm ơn bạn đã đọc!

Liên kết hữu ích