(1)释放 TypeScript 的力量:tsconfig 中的关键注意事项

 释放 TypeScript 的力量(系列 3 部分)

1、释放 TypeScript 的力量:tsconfig 中的关键注意事项

2、释放 TypeScript 的力量:改进标准库类型

3、让 TypeScript 真正成为“强类型"

如果您正在构建复杂的 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 的编译器在各种情况下可能会错误地处理nullundefined类型。其次,该any类型可能会不受控制地出现在代码库中,从而导致禁用该类型的类型检查。

幸运的是,通过调整配置中的一些选项,这些问题很容易解决。


严格模式

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

严格模式是一个重要的配置选项,它通过启用广泛的类型检查行为来为程序正确性提供更有力的保证。在 tsconfig 文件中启用严格模式是实现最大类型安全和更好的开发人员体验的关键一步。配置 tsconfig 需要一些额外的努力,但它可以在提高项目质量方面发挥很大作用。

编译strict器选项启用所有严格模式系列选项,其中包括noImplicitAnystrictNullChecksstrictFunctionTypes等。这些选项也可以单独配置,但不建议关闭其中任何一个。让我们通过示例来了解原因。


隐式任何推断

{
    "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[]

Catch 变量中的未知类型

{
    "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选项将错误类型从 切换为anyunknown以提醒我们在执行任何操作之前检查错误类型。

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'));
    }
}

err现在,TypeScript 将提示我们在将变量传递给函数之前检查变量的类型logError,从而生成更正确、更安全的代码。不幸的是,此选项无助于解决promise.catch()函数或回调函数中的键入错误。any但我们将在下一篇文章中讨论此类情况的处理方法。


调用和应用方法的类型检查

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

any另一个选项修复了通过call和进行函数内调用的外观apply。与前两种情况相比,这种情况不太常见,但仍然值得考虑。默认情况下,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编译器选项将提示我们显式指定函数的上下文类型。这样,在上面的示例中,我们可以捕获访问函数上下文而不是name类字段的错误Person


TypeScript 中的 Null 和未定义支持

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

接下来,该模式中包含的几个选项strict不会导致该any类型出现在代码库中。然而,它们使 TS 编译器的行为更加严格,并允许在开发过程中发现更多错误。

null第一个这样的选项修复了 TypeScript 中和的处理undefined。默认情况下,TypeScript 假定nullundefined是任何类型的有效值,这可能会导致意外的运行时错误。启用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

该代码编译时不会出现错误,但如果系统中不存在名为“Max”的用户,则可能会引发运行时错误,并users.find()返回undefined。为了防止这种情况,我们可以启用strictNullChecks编译器选项。

null现在,TypeScript 将迫使我们显式处理或被undefined返回的可能性users.find()

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,但它是另一个可以帮助提高项目中的代码质量的选项。它允许检查索引访问表达式是否具有nullundefined返回类型,这可以防止运行时错误。

考虑以下示例,其中我们有一个用于存储缓存值的对象。然后我们获取其中一个键的值。当然,我们不能保证所需键的值确实存在于缓存中。默认情况下,TypeScript 会假设该值存在并且类型为string。这可能会导致运行时错误。

const cache: Record = {};

const value = cache['key'];
//    ^?  string

console.log(value.toUpperCase());
//                ^  TypeError: Cannot read properties of undefined

在 TypeScript 中启用该noUncheckedIndexedAccess选项需要检查索引访问表达式的undefined返回类型,这可以帮助我们避免运行时错误。这也适用于访问数组中的元素。

const cache: Record = {};

const value = cache['key'];
//    ^?  string | undefined

if (value) {
    console.log(value.toUpperCase());
}

推荐配置

根据所讨论的选项,强烈建议在项目文件中启用strict和选项,以获得最佳类型安全性。noUncheckedIndexedAccesstsconfig.json

{
    "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 标准库中的类型来实现更好的类型安全和代码质量。请继续关注并感谢您的阅读!

你可能感兴趣的:(javascript,typescript)