如果您正在构建复杂的 Web 应用程序,TypeScript 可能是您选择的编程语言。 TypeScript 因其强大的类型系统和静态分析功能而广受喜爱,这使其成为确保代码健壮且无错误的强大工具。
它还通过与代码编辑器集成来加速开发过程,使开发人员能够更有效地导航代码并获得更准确的提示和自动完成,并能够安全地重构大量代码。
编译器是 TypeScript 的核心,负责检查类型正确性并将 TypeScript 代码转换为 JavaScript。然而,要充分利用 TypeScript 的强大功能,正确配置编译器非常重要。
每个 TypeScript 项目都有一个或多个tsconfig.json
文件,其中包含编译器的所有配置选项。
配置 tsconfig 是在 TypeScript 项目中实现最佳类型安全和开发人员体验的关键步骤。通过花时间仔细考虑涉及的所有关键因素,您可以加快开发过程并确保您的代码健壮且无错误。
tsconfig 中的默认配置可能会导致开发人员错过 TypeScript 的大部分好处。这是因为它无法启用许多强大的类型检查功能。 “默认”配置是指未设置类型检查编译器选项的配置。
例如:
{ "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }
由于两个主要原因,缺少几个关键配置选项可能会导致代码质量降低。首先,TypeScript 的编译器在各种情况下可能会错误地处理null
和undefined
类型。
其次, any
类型可能会不受控制地出现在代码库中,导致禁用该类型的类型检查。
幸运的是,通过调整配置中的一些选项,这些问题很容易解决。
{ "compilerOptions": { "strict": true } }
严格模式是一个重要的配置选项,它通过启用各种类型检查行为来为程序正确性提供更有力的保证。
在 tsconfig 文件中启用严格模式是实现最大类型安全和更好的开发人员体验的关键一步。
配置 tsconfig 需要一些额外的努力,但它可以在提高项目质量方面发挥很大作用。
strict
编译器选项启用所有严格模式系列选项,其中包括noImplicitAny
、 strictNullChecks
、 strictFunctionTypes
等。
这些选项也可以单独配置,但不建议关闭其中任何一个。让我们通过示例来了解原因。
{ "compilerOptions": { "noImplicitAny": true } }
any
类型是静态类型系统中的一个危险漏洞,使用它会禁用所有类型检查规则。结果,TypeScript 的所有好处都消失了:错误被遗漏,代码编辑器提示停止正常工作,等等。
仅在极端情况或出于原型设计需要时才可以使用any
。尽管我们尽了最大努力, any
类型有时仍会隐式潜入代码库。
默认情况下,编译器会原谅我们很多错误,以换取代码库中出现any
。具体来说,TypeScript 允许我们不指定变量的类型,即使类型无法自动推断也是如此。
问题是我们可能会意外地忘记指定变量的类型,例如函数参数的类型。 TypeScript 不会显示错误,而是自动将变量的类型推断为any
。
function parse(str) { // ^? any return str.split(''); } // TypeError: str.split is not a function const res1 = parse(42); const res2 = parse('hello'); // ^? any
启用noImplicitAny
编译器选项将导致编译器突出显示变量类型自动推断为any
所有位置。在我们的示例中,TypeScript 将提示我们指定函数参数的类型。
function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }
当我们指定类型时,TypeScript 会快速捕获将数字传递给字符串参数的错误。存储在变量res2
中的函数的返回值也将具有正确的类型。
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 } }
配置useUnknownInCatchVariables
可以安全处理 try-catch 块中的异常。默认情况下,TypeScript 假定 catch 块中的错误类型是any
,这允许我们对错误执行任何操作。
例如,我们可以将捕获的错误按原样传递给接受Error
实例的日志记录函数。
function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }
但实际上,错误的类型无法保证,我们只能在错误发生时在运行时确定其真实类型。如果日志记录函数接收到的不是Error
内容,这将导致运行时错误。
因此, useUnknownInCatchVariables
选项将错误类型从any
切换为unknown
,以提醒我们在执行任何操作之前检查错误类型。
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')); } }
现在,TypeScript 将提示我们在将err
变量传递给logError
函数之前检查其类型,从而生成更正确、更安全的代码。不幸的是,这个选项对处理promise.catch()
函数或回调函数中的键入错误没有帮助。
但我们将在下一篇文章中讨论处理any
情况的方法。
{ "compilerOptions": { "strictBindCallApply": true } }
另一个选项通过call
和apply
修复any
函数内调用的外观。与前两种情况相比,这种情况不太常见,但仍然值得考虑。默认情况下,TypeScript 根本不检查此类构造中的类型。
例如,我们可以将任何内容作为参数传递给函数,最后,我们将始终收到any
类型。
function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any
启用strictBindCallApply
选项使 TypeScript 更加智能,因此返回类型将被正确推断为number
。当尝试传递错误类型的参数时,TypeScript 会指出错误。
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 } }
下一个选项可以帮助防止项目中出现any
问题,修复函数调用中执行上下文的处理。 JavaScript 的动态特性使得静态确定函数内上下文的类型变得困难。
默认情况下,在这种情况下,TypeScript 使用any
类型作为上下文,并且不提供任何警告。
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. }; } }
启用noImplicitThis
编译器选项将提示我们显式指定函数的上下文类型。这样,在上面的示例中,我们可以捕获访问函数上下文而不是Person
类的name
字段的错误。
{ "compilerOptions": { "strictNullChecks": true } }
接下来, strict
模式中包含的几个选项不会导致any
类型出现在代码库中。然而,它们使 TS 编译器的行为更加严格,并允许在开发过程中发现更多错误。
第一个这样的选项修复了 TypeScript 中null
和undefined
的处理。默认情况下,TypeScript 假定null
和undefined
是任何类型的有效值,这可能会导致意外的运行时错误。
启用strictNullChecks
编译器选项会强制开发人员显式处理可能出现null
和undefined
情况。
例如,考虑以下代码:
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
这段代码编译时不会出现错误,但如果系统中不存在名为“Max”的用户,并且users.find()
返回undefined
,则可能会引发运行时错误。为了防止这种情况,我们可以启用strictNullChecks
编译器选项。
现在,TypeScript 将迫使我们显式处理users.find()
返回null
或undefined
的可能性。
const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }
通过显式处理null
和undefiined
的可能性,我们可以避免运行时错误并确保我们的代码更加健壮且无错误。
{ "compilerOptions": { "strictFunctionTypes": true } }
启用strictFunctionTypes
使 TypeScript 的编译器更加智能。在 2.6 版本之前,TypeScript 不检查函数参数的逆变性。如果使用错误类型的参数调用函数,这将导致运行时错误。
例如,即使一个函数类型能够处理字符串和数字,我们也可以将一个函数分配给只能处理字符串的类型。我们仍然可以将数字传递给该函数,但我们会收到运行时错误。
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);
幸运的是,启用strictFunctionTypes
选项可以修复此行为,并且编译器可以在编译时捕获这些错误,向我们显示函数中类型不兼容的详细消息。
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 } }
最后但并非最不重要的一点是, strictPropertyInitialization
选项允许检查不包含undefined
作为值的类型的强制类属性初始化。
例如,在下面的代码中,开发人员忘记初始化email
属性。默认情况下,TypeScript 不会检测到此错误,并且在运行时可能会出现问题。
class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }
然而,当启用strictPropertyInitialization
选项时,TypeScript 会为我们突出显示这个问题。
email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.
{ "compilerOptions": { "noUncheckedIndexedAccess": true } }
noUncheckedIndexedAccess
选项不是strict
模式的一部分,但它是另一个可以帮助提高项目中代码质量的选项。它允许检查索引访问表达式是否具有null
或undefined
返回类型,这可以防止运行时错误。
考虑以下示例,其中我们有一个用于存储缓存值的对象。然后我们获取其中一个键的值。当然,我们不能保证所需键的值确实存在于缓存中。
默认情况下,TypeScript 会假设该值存在并且类型为string
。这可能会导致运行时错误。
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined
在 TypeScript 中启用noUncheckedIndexedAccess
选项需要检查索引访问表达式是否undefined
返回类型,这可以帮助我们避免运行时错误。这也适用于访问数组中的元素。
const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }
根据讨论的选项,强烈建议在项目的tsconfig.json
文件中启用strict
和noUncheckedIndexedAccess
选项,以获得最佳类型安全性。
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }
如果您已经启用了strict
选项,您可以考虑删除以下选项以避免重复strict: true
选项:
noImplicitAny
useUnknownInCatchVariables
strictBindCallApply
noImplicitThis
strictFunctionTypes
strictNullChecks
strictPropertyInitialization
还建议删除以下可能削弱类型系统或导致运行时错误的选项:
keyofStringsOnly
noStrictGenericChecks
suppressImplicitAnyIndexErrors
suppressExcessPropertyErrors
通过仔细考虑和配置这些选项,您可以在 TypeScript 项目中实现最佳的类型安全性和更好的开发人员体验。
TypeScript 在其发展过程中已经取得了长足的进步,不断改进其编译器和类型系统。然而,为了保持向后兼容性,TypeScript 配置变得更加复杂,其中许多选项会显着影响类型检查的质量。
通过仔细考虑和配置这些选项,您可以在 TypeScript 项目中实现最佳的类型安全性和更好的开发人员体验。了解在项目配置中启用和删除哪些选项非常重要。
了解禁用某些选项的后果将使您能够为每个选项做出明智的决定。
重要的是要记住,严格的打字可能会产生后果。为了有效地处理 JavaScript 的动态特性,您需要对 TypeScript 有很好的理解,而不仅仅是在变量后面指定“数字”或“字符串”。
您将需要熟悉更复杂的构造以及 TypeScript 优先的库和工具生态系统,以更有效地解决将出现的与类型相关的问题。
因此,编写代码可能需要付出更多的努力,但根据我的经验,对于长期项目来说,这种努力是值得的。
我希望您从本文中学到了新的东西。这是该系列的第一部分。在下一篇文章中,我们将讨论如何通过改进 TypeScript 标准库中的类型来实现更好的类型安全和代码质量。请继续关注,感谢您的阅读!