TypeScript 如何帮助你编写更好的代码

如何使用TypeScript编写更好的代码

TypeScript正在接管Web。在本文中,我将为您概述一下TypeScript的好处,并向您展示它如何帮助您创建更少错误的网站。

您将了解到TypeScript如何帮助处理边缘情况、捕捉拼写错误、重构代码以及类型如何使代码更易于理解。

最近的JavaScript状况调查发现开发者花费的时间更多地用于编写TypeScript而不是JavaScript代码。GitHub自己的调查提出了一个较为适度的说法,称TypeScript仅位列该平台上使用最广泛的第4种语言 – 在JavaScript之后 – 但其使用量在一年内增长了近40%。为什么会出现这种转变呢?

part0
图表显示开发人员花在编写JavaScript与TypeScript上的时间比例

如果您更喜欢视频格式,您还可以观看该文章的视频版,其中内容更加丰富。

什么是类型?

您可能已经知道,在JavaScript(和其他编程语言)中,有各种数据类型,如字符串、数字、数组、对象、函数、布尔值、未定义和空值。这些都是类型。

但是JavaScript是动态类型的,这意味着变量的类型可以改变。您可以在一行中将变量的值设置为字符串,然后在另一行中将相同的变量设置为数字,例如:

let value = 'Hello';value = 3;

一方面,这是该语言的优点。这意味着它简单而非常灵活。您不必通过设置类型来限制自己。

另一方面,如果您不小心,很容易弄巧成拙。当您编写JavaScript代码时,使用正确类型的值是您的责任。

如果您意外使用了错误的类型,您将遇到错误。您尝试获取未定义变量的长度了吗?不要尝试,它会抛出一个错误。但也许您有一个边缘情况改变了您的值,而您没有意识到:

value = 3;...console.log(value.length); // TypeError: Cannot read properties of undefined

由于变量可以随时更改类型,JavaScript只在运行代码时检查代码是否正常工作。您只有在运行代码时才知道是否有错误。

如果您仅在非常罕见的边缘情况下出现错误,也许您甚至很长一段时间都没有意识到您的代码可能会失败。当您编写代码时,JavaScript不会警告您可能存在问题。

另一方面,TypeScript是静态类型的。这意味着变量无法更改类型。这使得代码更可预测,并允许TypeScript在您编写代码时分析代码并在发生错误时及时发现(因此它们不会显示为您代码中的错误)。

好了,现在您对类型是什么以及JavaScript和TypeScript在处理它们的方式上有何不同有了基本的了解,让我们深入探讨本教程主要部分。

您已经在使用TypeScript了

让我们回顾一下基本知识:TypeScript为您提供类型信息。它告诉您变量的类型,您需要传递给函数的参数类型,以及它们将返回何种数据类型。

但如果我告诉您,即使在普通的JavaScript中,您已经在使用TypeScript,您会怎么样呢?

让我们看一个快速的示例。这是一个在VS Code中的普通JavaScript文件。由于VS Code和TypeScript都由Microsoft制作,VS Code已经内置了TypeScript功能。

屏幕录制VS Code的简单JavaScript代码。一行中有一个名为'input'的变量,其值设置为字符串值。记录显示,当您将鼠标悬停在变量名上时,信息弹出窗口会正确显示其类型为字符串。它还显示,另一个名为'greeting'的变量被设置为函数的返回值。当悬停在这个变量名上时,它还显示其类型为字符串。最后,第三个名为'greetingLength'的变量被赋予了greeting的长度。当将鼠标悬停在其上时,会显示其值为数字。
当您将鼠标悬停在变量上时,VS Code会尽可能地告知其类型

“`html


function getGreeting(str) { return `Hello ${str}!`; }
let input = 'Bob'; // Type: string
let greeting = getGreeting(input); // Type: string
let greetingLength = greeting.length; // Type: number
console.log(greeting, greetingLength);

如果你将光标悬停在 input 变量上,VS Code 将告诉你它的类型是 string,这很清楚,因为我们在同一行中为它赋了一个字符串。

但是,如果我们进一步检查 greeting 的类型,它也会显示为 string。这有点有趣。Greeting 的值来自一个函数。我们怎么知道它是字符串呢?

在 TypeScript 的帮助下,VS Code 分析这个函数,检查每个可能的返回路径,并得出这个函数唯一可能返回的是一个字符串。

当然,这只是一个非常简单的例子。但是即使我们有一个更复杂的逻辑,有多个不同的返回语句,TypeScript 仍然会分析每个不同的路径,并告诉你可能的返回值是什么。

再多讲讲这个例子,如果我们将光标悬停在 length 变量上,会发现它的类型是一个 number。这可能看起来很明显,但是它背后的逻辑比它看起来聪明得多。

stringlength 属性是一个数字。但这仅在我们查看字符串时才是真实的。在这种情况下,我们知道它是字符串,因为 TypeScript 已经弄清楚了我们的函数的返回类型是一个字符串。背后有多个步骤。

所以 TypeScript 之所以强大的第一个原因是:你只需将光标悬停在值上,就可以了解其类型。这在纯 JavaScript 中也可以实现到某种程度。

检查内置函数所需参数

再看一个例子。这段代码仍然是 JavaScript,我们仍然没有明确定义类型,但是 TypeScript 解析器仍然可以推断出类型。

这里我们有一个输入值,又将其硬编码为 ‘bob’,然后将字符串大写。我们首先提取第一个字符,将其大写,然后再将字符串的剩余部分转换为小写。

我们可以检查这些函数的函数签名。我们可以看到,charAt 需要一个数字参数,toUpperCase 不需要任何参数,而 slice 函数有两个可选参数,可以是 numberundefined

这些都是 TypeScript 的注释。问号表示值是可选的,管道字符表示类型可以是这个或那个。

总之,我们知道我们的格式化输入是一个 string 类型。

JavaScript 无法再推断类型的地方

让我们将大写输入的逻辑提取到一个函数中。类型应该保持不变,对吗?嗯,不完全是这样。现在我们的大写输入的类型是 any

“`

function capitalize(str) {    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput = capitalize(input); // 类型:任意let formattedLength = formattedInput.length;

有趣!发生了什么?我们的函数应该总是返回一个字符串,对吧?如果你看整个代码库中的这个特例,那是的,应该返回一个字符串。

但是我们必须将函数独立起来看。我们不能假设它将接收什么。我们也可以将一个数字传给它,这种情况下它将失败。你无法读取数字的第一个字符或将其转换为小写或大写。

在JavaScript中我们不能声明在这里我们需要一个字符串,所以我们不能确定函数返回一个字符串。在TypeScript中,我们将指定此参数的类型以避免任何混淆。

您可能会注意到之前的示例与`getGreeting`函数不同。在那里,无论输入是什么,函数始终返回`字符串`。然而,在这里,输出取决于输入。

JavaScript中的边界情况如何处理?

我们能否避免在JavaScript中获取错误输入时出现错误?可以。

在函数失败之前检查类型并在函数之前返回是一种使我们的函数失效的方法。这仍然是JavaScript,`typeof`操作符是JavaScript的一部分。

现在,这个函数不会失败。但我引入了一个bug。

VS Code,与我们之前的简单JavaScript文件相同。只不过现在我们的capitalize函数只会在参数不是字符串时返回。该图显示返回值的类型从任意更改为字符串或undefined。
在我们的capitalize函数添加一个提前的return语句后,返回值的类型从any更改为string或undefined
function capitalize(str) {    if (typeof str !== 'string') return;    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput = capitalize(input); // 类型:字符串或undefinedlet formattedLength = formattedInput.length;

如果我们检查函数签名或新值的类型,就会发现它已经从`any`更改为`string | undefined`。

在某种程度上,这非常简洁。我们限制了可能的返回值,并且通过查看返回的值,我们更好地理解了函数的功能。但另一方面,如果它返回`undefined`,则应用程序将在下一行崩溃。您无法检查`undefined`的长度。

当然,我们也可以返回一个空字符串作为回退,然后就不会有这个问题了。我们在这里使用的是字符串。但这是一个很容易在JavaScript中忽视的主题的绝佳例子,它可能会给你带来很多麻烦:边界情况。

如果您没有编写capitalize函数,也不知道它的工作原理怎么办?

也许它还位于不同的文件中,您只是假设它将始终返回一个字符串。也许您使用的函数更长,更复杂。也许您只检查了它的最后一行并说’好吧,这个函数返回一个字符串’。但您完全忽略了在不同的行中 – 也可以在函数的中间 – 有另一个返回不同类型值的return语句。

这里的要点是边界情况可能会发生,很容易忽视它们。当开发人员涵盖应用程序的正常路径时,这是错误的典型来源,但在涵盖边界情况时他们较不细心。

在JavaScript中,您需要注意边界情况,测试不同的场景和用户输入,并编写彻底的单元测试,以确保您的逻辑不会失败。

如果编辑器能告诉您出了问题,那样不是很好吗?

将代码转换为TypeScript

因此,在这个简介之后,让我们最后将这段代码转换为TypeScript。为此,只需将扩展名从`。js`更改为`。ts`,然后看看会发生什么。

立即,我们将看到一个错误,说我们的`formattedInput`可能是`undefined`。这与获得该值的长度不匹配。我们已经捕获了之前导致问题的bug,而且我们甚至没有花时间为代码添加类型。

在我们先前的相同文件中,将文件扩展名更改为TypeScript。该图片显示出现了一个新的错误。
将扩展名更改为.ts后,出现了一个错误,表明我们的大写值可能未定义
function capitalize(str) { if (typeof str !== 'string') return; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput = capitalize(input);let formattedLength = formattedInput.length; // 错误

在解决这个错误之前,让我们打开strict模式。默认情况下,TypeScript可以非常宽松,但在没有strict模式的情况下,我们也无法从中获得更多的价值。

为此,我们需要在项目的根目录下创建一个tsconfig.json文件。此时可能会有些让人吓到,但是在使用任何框架创建项目时,该文件很可能已经自动生成了。现在重要的是我们将strict模式设置为true。

{ "compilerOptions": { "target": "ESNext", "module": "ESNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "lib": ["DOM", "ESNext"], "moduleResolution": "node", "noImplicitAny": true, "allowSyntheticDefaultImports": true, "baseUrl": "./src", "paths": { "*": ["src/*"] }, "outDir": "./dist", "rootDir": "./src", "strictPropertyInitialization": false, "noEmit": false, }, "include": ["src/**/*.ts", "src/index.js"], "exclude": ["node_modules"] }

这将显示更多的错误,因为在这种设置下,我们必须定义函数参数的类型。

VS Code与先前的相同TypeScript文件。现在启用严格模式后,出现了另一个错误,指出我们需要设置函数参数的类型。
打开严格模式后,我们需要设置函数参数的类型。
function capitalize(str) { // 错误 if (typeof str !== 'string') return; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput = capitalize(input);let formattedLength = formattedInput.length; // 错误

因此,让我们通过将参数设置为str: string来指定我们的capitalize函数需要一个string。在这种情况下,这就是我们需要添加的所有类型,因为这是TypeScript无法自动推断的唯一类型。

VS Code与先前的相同TypeScript文件。除了我们将函数参数的类型设置为string。
将函数参数的类型设置为string
function capitalize(str: string) { if (typeof str !== 'string') return; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput = capitalize(input);let formattedLength = formattedInput.length; // 错误

关于TypeScript的一个误解是你必须为所有内容添加类型。虽然这不是一个坏习惯,但它并不是绝对必要的。TypeScript非常智能。它会分析代码并尽可能推断出尽可能多的类型。

当然,我们也可以在其他地方指定类型。我们可以通过将formattedInput的值设置为string来指定我们需要的类型,即let formattedInput: string。这是我们的整个问题所在。我们认为它是一个字符串,但在某些情况下,它实际上不是。

VS Code与先前的相同TypeScript文件。除了现在我们设置了将capitalize函数返回值的变量类型为字符串。这揭示出可能存在类型不匹配,因为capitalize函数可能返回undefined。
设置变量’formattedInput’的类型后,错误发生了变化。
function capitalize(str: string) {    if (typeof str !== 'string') return;    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput: string = capitalize(input); // 错误let formattedLength = formattedInput.length;

这立刻突出了我们的问题。我们希望它是一个 string,但是我们的函数可能会返回 undefined。我们可以在弹出窗口中阅读到 undefined 不能被赋值给类型 string

我们可以进一步说,我们希望该函数返回一个 string。这将再次改变错误。现在问题不是无法将返回值赋给 string 变量,而是函数本身返回了错误的值。

与之前相同的 TypeScript 文件的 VS Code。除了现在我们将 capitalize 函数的返回类型设置为 string。这改变了我们的错误,因为该函数可能返回 undefined。
设置 capitalize 函数的返回类型后,错误再次改变
function capitalize(str: string): string {    if (typeof str !== 'string') return; // 错误    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}let input = 'bob';let formattedInput: string = capitalize(input);let formattedLength = formattedInput.length;

为了解决这个问题,让我们删除整行代码。之前添加这一行是为了类型检查,但现在我们让 TypeScript 为我们进行整个类型检查。函数签名已经说明了该属性必须是一个 string,没有必要再次检查它。我们的代码变得更简单,同时更安全。

所以 TypeScript 令人惊叹的另一个原因是它迫使你考虑边缘情况。不仅要考虑边缘情况,还要处理它们。在 JavaScript 中很容易忽视这一点。

重构代码

现在我们已经了解了基础知识,让我们进入第三个主题:重构。让我们稍微修改一下我们的问候函数,假设现在它接受两个参数:名字和姓氏。想象一下这是一个在一个庞大复杂的项目中广泛使用的实用程序函数:

具有一个简单 TypeScript 文件的 VS Code。在这个文件中,我们定义了一个带有两个参数(名字和姓氏)的 'getGreeting' 函数。这两个参数的类型都是字符串。
我们新的更新过的 ‘getGreeting’ 函数,接收名字和姓氏这两个参数
export function getGreeting(firstName: string, lastName: string) {    const formattedFirstName = capitalize(firstName);    const formattedLastName = capitalize(lastName);    return `Hello ${formattedFirstName} ${formattedLastName}!`;}function capitalize(str: string): string {    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}

如果我们决定需要重构这段代码怎么办?我们想要传递一个具有名字和姓氏属性的对象,而不是传递两个字符串。

在 TypeScript 中,我们可以精确地定义对象的形状。我们可以定义我们必须传递一个 person 参数,它应该是一个具有 firstNamelastName 属性的对象,这两个属性都必须是 字符串

我们可以为此参数定义一个类型。我们说我们有一个以大写 P 的 Person 类型,按照惯例。这个类型描述了一个具有 firstNamelastName 属性的对象。

我们甚至可以添加更多的内容,比如添加一个类型为 Datebirthday 属性。但是让我们将其设为可选,因为我们现在不想处理它。

在这里添加一个问号会使这个属性变为可选项。我们可以设置它,但不是必须。但是当我们尝试使用它时,也不能假设它是存在的。

具有相同 TypeScript 文件的 VS Code。除了现在我们像这样定义了一个自定义 person 类型:`type Person = { firstName: string, lastName: string, birthday?: Date }`。然后我们将两个函数参数更改为一个具有类型 `Person` 的单独的 'person' 参数。结果是,编辑器会在这个文件中显示错误,并且还会在文件资源管理器中突出显示其他文件有错误。
更改函数参数类型后,编辑器会突出显示我们需要更改的部分

type Person = {    firstName: string,    lastName: string,    birthDay?: Date}export function getGreeting(person: Person) {    const formattedFirstName = capitalize(firstName);    const formattedLastName = capitalize(lastName);    return `你好 ${formattedFirstName} ${formattedLastName}!`;}function capitalize(str: string): string {    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}

现在,我们可以指定我们的person参数的类型为Person

当我们做出这个改变时,编辑器就会变红。它表示我正在尝试使用不存在的变量。在这个函数中,我引用了firstNamelastName,而现在只有一个person对象。

此外,文件浏览器中的其他文件也会变红,表示我调用了带有两个参数的函数,而它只期望有一个参数。

让我们修复这个文件中的错误,将firstNamelastName替换为person.firstNameperson.lastName。TypeScript非常严格地要求使用存在的变量。

让我们举个更好的例子:如果我在这里打错了一个字母怎么办?如果我从firstName中漏掉一个字母,这在JavaScript中可能是一个很容易忽视的问题。在这里,不仅会强调Person上没有这样的属性,甚至还建议你可能想使用firstName

VS Code中相同的TypeScript文件。除了我们更新了函数以使用新的函数参数。由于错误,我们打了一个错字。编辑器会突出显示错字并提供更正建议。
如果我们打错字,编辑器会提供更正建议
type Person = {    firstName: string,    lastName: string,    birthDay?: Date}export function getGreeting(person: Person) {    const formattedFirstName = capitalize(person.frstName);    const formattedLastName = capitalize(person.lastName);    return `你好 ${formattedFirstName} ${formattedLastName}!`;}function capitalize(str: string): string {    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();}

然后,让我们修复其他文件中的错误。正如你所见,造成错误的文件已经在文件浏览器中被突出显示了。这当然是一个非常简单的例子,但是想象一下,你有一个庞大的项目,这个函数在一百个不同的地方被调用。当然,你可以一丝不苟地逐个修复它们,但是在JavaScript中,很容易漏掉其中的一个。

VS Code中的另一个TypeScript文件调用我们之前定义的函数。当我们从之前的两个参数调用方式更新函数调用时,TypeScript会给出相关错误提示。
更新函数调用时,TypeScript会给出错误提示
import { getGreeting } from "./utils";let greeting = getGreeting({ firstName: 'bob', lastName:  'marley' });console.log(greeting);

现在,这里的错误提示说我们传递了两个参数,但只有一个参数是期望的。如果我们只删除第二个参数,它会说我们传递了一个字符串,但是它期望一个类型为Person的对象。如果我们只传递一个只有名字的对象,它仍然会抱怨我们缺少姓氏。如果我们添加了姓氏,但是又打错了一个字母,它会说我们有一个错误的属性,甚至还会建议我们可能在这里打错了字。

TypeScript非常精确地指出了我们的问题所在,我们可以很容易地找到如何修复它。

现在让我们修复另一个文件。我们可以将参数定义为变量,TypeScript将会认识到一个形状匹配这个函数的对象。

如果我们想确定我们的变量是Person类型,我们也可以导入这个类型,并将其设置为这个对象。首先,在实用文件中,我们需要导出它,然后就可以像导入函数一样使用它,然后将它赋值给我们的对象。

VS Code与另一个TypeScript文件调用我们之前定义的函数。这次我们没有内联函数参数,而是为其创建了一个新变量。在设置这个参数的类型时,我们重用了之前定义的'Person'类型。
我们也可以在其他文件中使用我们的Person类型
import { Person, getGreeting } from "./utils";let person: Person = {    firstName: 'bob',    lastName: 'dylan'}let greeting = getGreeting(person);console.log(greeting);

总结

TypeScript可能比这更加复杂。但这就是要点。在大部分情况下,你定义自己的类型,TypeScript会确保你正确使用它们。

总结一下,使用TypeScript有三个主要原因:

  1. 你可以获取函数的类型信息
  2. 你知道它们返回什么
  3. 你知道它们对你的期望,即使不看函数本身

TypeScript确保你正确地连接应用程序。它强制你使用正确的参数调用函数,并迫使你考虑边界情况。

TypeScript在重构过程中帮助很多。它不会让你遗漏需要更改的代码部分,也不会放过拼写错误。

订阅更多有关Web开发的教程:

Hunor Márton Borbély使用JavaScript进行游戏开发、创意编码教程、HTML画布、SVG、Three.js,以及一些React和Vue https://twitter.com/HunorBorbelyhttps://codepen.io/HunorMarton…favicon_144x144YouTubeAPkrFKaQ34YAITK6J0qgy6Iv6pms35dPhF68Hyy7BoYoLA=s900-c-k-c0x00ffffff-no-rj

Leave a Reply

Your email address will not be published. Required fields are marked *