paint-brush
Giải phóng sức mạnh của TypeScript: Những cân nhắc chính trong tsconfigtừ tác giả@nodge
2,187 lượt đọc
2,187 lượt đọc

Giải phóng sức mạnh của TypeScript: Những cân nhắc chính trong tsconfig

từ tác giả Maksim Zemskov13m2023/07/12
Read on Terminal Reader

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

TypeScript là ngôn ngữ phổ biến để xây dựng các ứng dụng phức tạp, nhờ hệ thống kiểu mạnh và khả năng phân tích tĩnh. Tuy nhiên, để đạt được mức an toàn tối đa cho loại, điều quan trọng là phải định cấu hình tsconfig chính xác. Trong bài viết này, chúng ta sẽ thảo luận về những cân nhắc chính để định cấu hình tsconfig nhằm đạt được sự an toàn tối ưu cho loại.
featured image - Giải phóng sức mạnh của TypeScript: Những cân nhắc chính trong tsconfig
Maksim Zemskov HackerNoon profile picture
0-item
1-item

Nếu bạn đang xây dựng các ứng dụng web phức tạp, TypeScript có thể là ngôn ngữ lập trình mà bạn lựa chọn. TypeScript được yêu thích nhờ hệ thống kiểu mạnh và khả năng phân tích tĩnh, làm cho nó trở thành một công cụ mạnh mẽ để đảm bảo rằng mã của bạn mạnh mẽ và không có lỗi.


Nó cũng tăng tốc quá trình phát triển thông qua tích hợp với trình chỉnh sửa mã, cho phép các nhà phát triển điều hướng mã hiệu quả hơn và nhận được các gợi ý và tự động hoàn thành chính xác hơn, cũng như cho phép tái cấu trúc an toàn một lượng lớn mã.


Trình biên dịch là trung tâm của TypeScript, chịu trách nhiệm kiểm tra tính chính xác của loại và chuyển đổi mã TypeScript thành JavaScript. Tuy nhiên, để sử dụng hết sức mạnh của TypeScript, điều quan trọng là phải cấu hình đúng Trình biên dịch.


Mỗi dự án TypeScript có một hoặc nhiều tệp tsconfig.json chứa tất cả các tùy chọn cấu hình cho Trình biên dịch.


Định cấu hình tsconfig là một bước quan trọng để đạt được trải nghiệm nhà phát triển và an toàn loại tối ưu trong các dự án TypeScript của bạn. Bằng cách dành thời gian để xem xét cẩn thận tất cả các yếu tố chính liên quan, bạn có thể đẩy nhanh quá trình phát triển và đảm bảo rằng mã của bạn mạnh mẽ và không có lỗi.

Nhược điểm của cấu hình tiêu chuẩn

Cấu hình mặc định trong tsconfig có thể khiến các nhà phát triển bỏ lỡ phần lớn lợi ích của TypeScript. Điều này là do nó không kích hoạt nhiều khả năng kiểm tra kiểu mạnh mẽ. Theo cấu hình "mặc định", ý tôi là cấu hình không có tùy chọn trình biên dịch kiểm tra kiểu nào được đặt.


Ví dụ:


 { "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }


Việc không có một số tùy chọn cấu hình chính có thể dẫn đến chất lượng mã thấp hơn vì hai lý do chính. Thứ nhất, trình biên dịch của TypeScript có thể xử lý không chính xác các loại nullundefined trong nhiều trường hợp.


Thứ hai, any loại nào có thể xuất hiện không kiểm soát được trong cơ sở mã của bạn, dẫn đến việc kiểm tra loại bị vô hiệu hóa xung quanh loại này.


May mắn thay, những sự cố này rất dễ khắc phục bằng cách điều chỉnh một số tùy chọn trong cấu hình.

Chế độ nghiêm ngặt

 { "compilerOptions": { "strict": true } }


Chế độ nghiêm ngặt là một tùy chọn cấu hình thiết yếu giúp đảm bảo chắc chắn hơn về tính chính xác của chương trình bằng cách cho phép một loạt các hành vi kiểm tra loại.


Kích hoạt chế độ nghiêm ngặt trong tệp tsconfig là một bước quan trọng để đạt được mức độ an toàn tối đa cho loại và trải nghiệm nhà phát triển tốt hơn.


Nó đòi hỏi thêm một chút nỗ lực trong việc định cấu hình tsconfig, nhưng nó có thể giúp ích rất nhiều trong việc cải thiện chất lượng dự án của bạn.


Tùy chọn trình biên dịch strict cho phép tất cả các tùy chọn họ chế độ nghiêm ngặt, bao gồm noImplicitAny , strictNullChecks , strictFunctionTypes , trong số những tùy chọn khác.


Các tùy chọn này cũng có thể được định cấu hình riêng nhưng không nên tắt bất kỳ tùy chọn nào trong số chúng. Hãy xem xét các ví dụ để biết lý do tại sao.

Tiềm ẩn mọi suy luận

 { "compilerOptions": { "noImplicitAny": true } }


Kiểu any là một kẽ hở nguy hiểm trong hệ thống kiểu tĩnh và việc sử dụng nó sẽ vô hiệu hóa tất cả các quy tắc kiểm tra kiểu. Kết quả là, tất cả các lợi ích của TypeScript đều bị mất: các lỗi bị bỏ qua, các gợi ý của trình chỉnh sửa mã ngừng hoạt động bình thường, v.v.


Chỉ sử dụng any trong trường hợp cực đoan hoặc cho nhu cầu tạo mẫu. Bất chấp những nỗ lực tốt nhất của chúng tôi, any loại nào đôi khi có thể ngấm ngầm xâm nhập vào cơ sở mã.


Theo mặc định, trình biên dịch tha thứ cho chúng tôi rất nhiều lỗi để đổi lấy sự xuất hiện của any trong cơ sở mã. Cụ thể, TypeScript cho phép chúng tôi không chỉ định loại biến, ngay cả khi loại đó không thể được suy ra tự động.


Vấn đề là chúng ta có thể vô tình quên chỉ định loại biến, chẳng hạn như đối số hàm. Thay vì hiển thị lỗi, TypeScript sẽ tự động suy ra loại biến là any .


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


Kích hoạt tùy chọn trình biên dịch noImplicitAny sẽ khiến trình biên dịch đánh dấu tất cả các vị trí mà loại biến được tự động suy ra là any . Trong ví dụ của chúng tôi, TypeScript sẽ nhắc chúng tôi chỉ định loại cho đối số hàm.


 function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }


Khi chúng tôi chỉ định loại, TypeScript sẽ nhanh chóng bắt lỗi khi chuyển một số thành một tham số chuỗi. Giá trị trả về của hàm, được lưu trữ trong biến res2 , cũng sẽ có kiểu chính xác.


 function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]


Loại không xác định trong biến Catch

 { "compilerOptions": { "useUnknownInCatchVariables": true } }


Định cấu hình useUnknownInCatchVariables cho phép xử lý an toàn các ngoại lệ trong các khối thử bắt. Theo mặc định, TypeScript giả định rằng loại lỗi trong khối bắt là any , cho phép chúng tôi làm bất cứ điều gì với lỗi.


Ví dụ: chúng ta có thể chuyển nguyên trạng lỗi đã bắt được cho một chức năng ghi nhật ký chấp nhận một phiên bản của Error .


 function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }


Tuy nhiên, trên thực tế, không có gì đảm bảo về loại lỗi và chúng ta chỉ có thể xác định đúng loại của nó trong thời gian chạy khi xảy ra lỗi. Nếu chức năng ghi nhật ký nhận được thứ gì đó không phải là Error , điều này sẽ dẫn đến lỗi thời gian chạy.


Do đó, tùy chọn useUnknownInCatchVariables chuyển loại lỗi từ any sang unknown để nhắc chúng tôi kiểm tra loại lỗi trước khi thực hiện bất kỳ điều gì với nó.


 try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }


Bây giờ, TypeScript sẽ nhắc chúng ta kiểm tra loại biến err trước khi chuyển nó đến hàm logError , dẫn đến mã chính xác hơn và an toàn hơn. Thật không may, tùy chọn này không giúp sửa lỗi đánh máy trong các hàm promise.catch() hoặc hàm gọi lại.


Tuy nhiên, chúng ta sẽ thảo luận về cách đối phó với any trường hợp nào trong những trường hợp như vậy trong bài viết tiếp theo.

Nhập Kiểm tra các phương thức gọi và áp dụng

 { "compilerOptions": { "strictBindCallApply": true } }


Một tùy chọn khác khắc phục sự xuất hiện của any lệnh gọi trong chức năng nào thông qua callapply . Đây là trường hợp ít phổ biến hơn hai trường hợp đầu tiên, nhưng vẫn cần xem xét. Theo mặc định, TypeScript hoàn toàn không kiểm tra các loại trong các cấu trúc như vậy.


Ví dụ: chúng ta có thể chuyển bất kỳ thứ gì làm đối số cho hàm và cuối cùng, chúng ta sẽ luôn nhận được loại any .


 function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any


Kích hoạt tùy strictBindCallApply làm cho TypeScript thông minh hơn, do đó, kiểu trả về sẽ được suy ra chính xác là number . Và khi cố gắng chuyển một đối số sai loại, TypeScript sẽ chỉ ra lỗi.


 function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.


Các loại nghiêm ngặt cho bối cảnh thực thi

 { "compilerOptions": { "noImplicitThis": true } }


Tùy chọn tiếp theo có thể giúp ngăn chặn sự xuất hiện của any trong dự án của bạn, khắc phục việc xử lý ngữ cảnh thực thi trong các lời gọi hàm. Bản chất động của JavaScript khiến việc xác định tĩnh loại ngữ cảnh bên trong một hàm trở nên khó khăn.


Theo mặc định, TypeScript sử dụng loại any cho ngữ cảnh trong những trường hợp như vậy và không đưa ra bất kỳ cảnh báo nào.


 class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }


Kích hoạt tùy chọn trình biên dịch noImplicitThis sẽ nhắc chúng ta chỉ định rõ ràng loại ngữ cảnh cho một hàm. Bằng cách này, trong ví dụ trên, chúng ta có thể bắt lỗi khi truy cập vào ngữ cảnh chức năng thay vì trường name của lớp Person .


Hỗ trợ Null và Undefined trong TypeScript

 { "compilerOptions": { "strictNullChecks": true } }


Tiếp theo, một số tùy chọn được bao gồm trong chế độ strict không dẫn đến any loại nào xuất hiện trong cơ sở mã. Tuy nhiên, chúng làm cho hành vi của trình biên dịch TS chặt chẽ hơn và cho phép tìm thấy nhiều lỗi hơn trong quá trình phát triển.


Tùy chọn đầu tiên như vậy sửa lỗi xử lý nullundefined trong TypeScript. Theo mặc định, TypeScript giả định rằng nullundefined là các giá trị hợp lệ cho bất kỳ loại nào, điều này có thể dẫn đến lỗi thời gian chạy không mong muốn.


Kích hoạt tùy chọn trình biên strictNullChecks buộc nhà phát triển phải xử lý rõ ràng các trường hợp có thể xảy ra nullundefined .


Ví dụ: hãy xem xét đoạn mã sau:


 const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined


Mã này sẽ biên dịch mà không có lỗi, nhưng nó có thể gây ra lỗi thời gian chạy nếu người dùng có tên “Max” không tồn tại trong hệ thống và users.find() trả về undefined . Để ngăn chặn điều này, chúng ta có thể kích hoạt tùy chọn trình biên strictNullChecks .


Bây giờ, TypeScript sẽ buộc chúng ta xử lý rõ ràng khả năng null hoặc undefined được trả về bởi users.find() .


 const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }


Bằng cách xử lý rõ ràng khả năng của nullundefiined , chúng ta có thể tránh được các lỗi thời gian chạy và đảm bảo rằng mã của chúng ta mạnh mẽ hơn và không có lỗi.

Các loại chức năng nghiêm ngặt

 { "compilerOptions": { "strictFunctionTypes": true } }


Kích hoạt strictFunctionTypes làm cho trình biên dịch của TypeScript thông minh hơn. Trước phiên bản 2.6, TypeScript không kiểm tra tính trái ngược của các đối số hàm. Điều này sẽ dẫn đến lỗi thời gian chạy nếu hàm được gọi với đối số sai loại.


Ví dụ: ngay cả khi một loại hàm có khả năng xử lý cả chuỗi và số, chúng ta có thể gán một hàm cho loại đó chỉ có thể xử lý chuỗi. Chúng tôi vẫn có thể chuyển một số cho chức năng đó, nhưng chúng tôi sẽ nhận được lỗi thời gian chạy.


 function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);


May mắn thay, việc bật tùy strictFunctionTypes sẽ khắc phục hành vi này và trình biên dịch có thể phát hiện các lỗi này tại thời điểm biên dịch, hiển thị cho chúng tôi thông báo chi tiết về kiểu không tương thích trong các hàm.


 const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.


Khởi tạo thuộc tính lớp

 { "compilerOptions": { "strictPropertyInitialization": true } }


Cuối cùng nhưng không kém phần quan trọng, tùy strictPropertyInitialization cho phép kiểm tra việc khởi tạo thuộc tính lớp bắt buộc đối với các loại không bao gồm giá trị undefined .


Ví dụ: trong đoạn mã sau, nhà phát triển đã quên khởi tạo thuộc tính email . Theo mặc định, TypeScript sẽ không phát hiện ra lỗi này và có thể xảy ra sự cố khi chạy.


 class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }


Tuy nhiên, khi tùy strictPropertyInitialization được bật, TypeScript sẽ làm nổi bật vấn đề này cho chúng tôi.


 email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.

Chữ ký chỉ mục an toàn

 { "compilerOptions": { "noUncheckedIndexedAccess": true } }


Tùy chọn noUncheckedIndexedAccess không phải là một phần của chế độ strict , nhưng nó là một tùy chọn khác có thể giúp cải thiện chất lượng mã trong dự án của bạn. Nó cho phép kiểm tra các biểu thức truy cập chỉ mục có kiểu trả về null hoặc undefined , điều này có thể ngăn lỗi thời gian chạy.


Xem xét ví dụ sau, nơi chúng tôi có một đối tượng để lưu trữ các giá trị được lưu trong bộ nhớ cache. Sau đó, chúng tôi nhận được giá trị cho một trong các khóa. Tất nhiên, chúng tôi không đảm bảo rằng giá trị cho khóa mong muốn thực sự tồn tại trong bộ đệm.


Theo mặc định, TypeScript sẽ cho rằng giá trị đó tồn tại và có kiểu string . Điều này có thể dẫn đến lỗi thời gian chạy.


 const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined


Việc bật tùy chọn noUncheckedIndexedAccess trong TypeScript yêu cầu kiểm tra các biểu thức truy cập chỉ mục để biết loại trả về undefined , điều này có thể giúp chúng tôi tránh các lỗi thời gian chạy. Điều này cũng áp dụng cho việc truy cập các phần tử trong một mảng.


 const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }

Cấu hình đề xuất

Dựa trên các tùy chọn đã thảo luận, chúng tôi khuyên bạn nên bật các tùy chọn strictnoUncheckedIndexedAccess trong tệp tsconfig.json của dự án để đảm bảo an toàn cho loại tối ưu.


 { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }


Nếu bạn đã bật tùy chọn strict , bạn có thể cân nhắc xóa các tùy chọn sau để tránh trùng lặp tùy chọn strict: true :


  • noImplicitAny
  • useUnknownInCatchVariables
  • strictBindCallApply
  • noImplicitThis
  • strictFunctionTypes
  • strictNullChecks
  • strictPropertyInitialization


Bạn cũng nên xóa các tùy chọn sau đây có thể làm suy yếu hệ thống loại hoặc gây ra lỗi thời gian chạy:


  • keyofStringsOnly
  • noStrictGenericChecks
  • suppressImplicitAnyIndexErrors
  • suppressExcessPropertyErrors


Bằng cách xem xét và định cấu hình cẩn thận các tùy chọn này, bạn có thể đạt được độ an toàn loại tối ưu và trải nghiệm nhà phát triển tốt hơn trong các dự án TypeScript của mình.

Phần kết luận

TypeScript đã trải qua một chặng đường dài trong quá trình phát triển của nó, không ngừng cải thiện trình biên dịch và hệ thống kiểu của nó. Tuy nhiên, để duy trì khả năng tương thích ngược, cấu hình TypeScript đã trở nên phức tạp hơn, với nhiều tùy chọn có thể ảnh hưởng đáng kể đến chất lượng kiểm tra kiểu.


Bằng cách xem xét và định cấu hình cẩn thận các tùy chọn này, bạn có thể đạt được độ an toàn loại tối ưu và trải nghiệm nhà phát triển tốt hơn trong các dự án TypeScript của mình. Điều quan trọng là phải biết tùy chọn nào để bật và xóa khỏi cấu hình dự án.


Hiểu được hậu quả của việc vô hiệu hóa một số tùy chọn nhất định sẽ cho phép bạn đưa ra quyết định sáng suốt cho từng tùy chọn.


Điều quan trọng cần lưu ý là việc gõ nghiêm ngặt có thể gây ra hậu quả. Để giải quyết hiệu quả bản chất động của JavaScript, bạn cần hiểu rõ về TypeScript ngoài việc chỉ định "số" hoặc "chuỗi" sau một biến.


Bạn sẽ cần làm quen với các cấu trúc phức tạp hơn và hệ sinh thái thư viện và công cụ đầu tiên của TypeScript để giải quyết hiệu quả hơn các vấn đề liên quan đến kiểu sẽ phát sinh.


Do đó, việc viết mã có thể cần nhiều nỗ lực hơn một chút, nhưng dựa trên kinh nghiệm của tôi, nỗ lực này rất đáng giá đối với các dự án dài hạn.


Tôi hy vọng bạn đã học được một cái gì đó mới từ bài viết này. Đây là phần đầu tiên của một loạt. Trong bài viết tiếp theo, chúng ta sẽ thảo luận cách đạt được chất lượng mã và an toàn kiểu tốt hơn bằng cách cải thiện các kiểu trong thư viện chuẩn của TypeScript. Hãy theo dõi, và cảm ơn vì đã đọc!

Liên kết hữu ích