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.
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 null
và undefined
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.
{ "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.
{ "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[]
{ "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.
{ "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 call
và apply
. Đâ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'.
{ "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
.
{ "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ý null
và undefined
trong TypeScript. Theo mặc định, TypeScript giả định rằng null
và undefined
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 null
và undefined
.
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 null
và undefiined
, 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.
{ "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'.
{ "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.
{ "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()); }
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 strict
và noUncheckedIndexedAccess
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.
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!