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ã.
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:
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:
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
.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.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.
Đ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ã:
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 tsconfig và Cả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ã.
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
.
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.
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 pokemons
và settings
đề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()
và 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.
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ố đó.
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);
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
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 đề:
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.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); }
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)); }
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.
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!