TypeScript 如何帮助你编写更好的代码
如何使用TypeScript编写更好的代码
TypeScript正在接管Web。在本文中,我将为您概述一下TypeScript的好处,并向您展示它如何帮助您创建更少错误的网站。
您将了解到TypeScript如何帮助处理边缘情况、捕捉拼写错误、重构代码以及类型如何使代码更易于理解。
最近的JavaScript状况调查发现开发者花费的时间更多地用于编写TypeScript而不是JavaScript代码。GitHub自己的调查提出了一个较为适度的说法,称TypeScript仅位列该平台上使用最广泛的第4种语言 – 在JavaScript之后 – 但其使用量在一年内增长了近40%。为什么会出现这种转变呢?
如果您更喜欢视频格式,您还可以观看该文章的视频版,其中内容更加丰富。
什么是类型?
您可能已经知道,在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功能。
“`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
。这可能看起来很明显,但是它背后的逻辑比它看起来聪明得多。
string
的 length
属性是一个数字。但这仅在我们查看字符串时才是真实的。在这种情况下,我们知道它是字符串,因为 TypeScript 已经弄清楚了我们的函数的返回类型是一个字符串。背后有多个步骤。
所以 TypeScript 之所以强大的第一个原因是:你只需将光标悬停在值上,就可以了解其类型。这在纯 JavaScript 中也可以实现到某种程度。
检查内置函数所需参数
再看一个例子。这段代码仍然是 JavaScript,我们仍然没有明确定义类型,但是 TypeScript 解析器仍然可以推断出类型。
这里我们有一个输入值,又将其硬编码为 ‘bob’,然后将字符串大写。我们首先提取第一个字符,将其大写,然后再将字符串的剩余部分转换为小写。
我们可以检查这些函数的函数签名。我们可以看到,charAt
需要一个数字参数,toUpperCase
不需要任何参数,而 slice
函数有两个可选参数,可以是 number
或 undefined
。
这些都是 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。
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,而且我们甚至没有花时间为代码添加类型。
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"] }
这将显示更多的错误,因为在这种设置下,我们必须定义函数参数的类型。
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无法自动推断的唯一类型。
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
。这是我们的整个问题所在。我们认为它是一个字符串,但在某些情况下,它实际上不是。
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
变量,而是函数本身返回了错误的值。
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 中很容易忽视这一点。
重构代码
现在我们已经了解了基础知识,让我们进入第三个主题:重构。让我们稍微修改一下我们的问候函数,假设现在它接受两个参数:名字和姓氏。想象一下这是一个在一个庞大复杂的项目中广泛使用的实用程序函数:
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
参数,它应该是一个具有 firstName
和 lastName
属性的对象,这两个属性都必须是 字符串
。
我们可以为此参数定义一个类型。我们说我们有一个以大写 P 的 Person
类型,按照惯例。这个类型描述了一个具有 firstName
和 lastName
属性的对象。
我们甚至可以添加更多的内容,比如添加一个类型为 Date
的 birthday
属性。但是让我们将其设为可选,因为我们现在不想处理它。
在这里添加一个问号会使这个属性变为可选项。我们可以设置它,但不是必须。但是当我们尝试使用它时,也不能假设它是存在的。
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
。
当我们做出这个改变时,编辑器就会变红。它表示我正在尝试使用不存在的变量。在这个函数中,我引用了firstName
和lastName
,而现在只有一个person
对象。
此外,文件浏览器中的其他文件也会变红,表示我调用了带有两个参数的函数,而它只期望有一个参数。
让我们修复这个文件中的错误,将firstName
和lastName
替换为person.firstName
和person.lastName
。TypeScript非常严格地要求使用存在的变量。
让我们举个更好的例子:如果我在这里打错了一个字母怎么办?如果我从firstName
中漏掉一个字母,这在JavaScript中可能是一个很容易忽视的问题。在这里,不仅会强调Person
上没有这样的属性,甚至还建议你可能想使用firstName
。
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中,很容易漏掉其中的一个。
import { getGreeting } from "./utils";let greeting = getGreeting({ firstName: 'bob', lastName: 'marley' });console.log(greeting);
现在,这里的错误提示说我们传递了两个参数,但只有一个参数是期望的。如果我们只删除第二个参数,它会说我们传递了一个字符串,但是它期望一个类型为Person
的对象。如果我们只传递一个只有名字的对象,它仍然会抱怨我们缺少姓氏。如果我们添加了姓氏,但是又打错了一个字母,它会说我们有一个错误的属性,甚至还会建议我们可能在这里打错了字。
TypeScript非常精确地指出了我们的问题所在,我们可以很容易地找到如何修复它。
现在让我们修复另一个文件。我们可以将参数定义为变量,TypeScript将会认识到一个形状匹配这个函数的对象。
如果我们想确定我们的变量是Person类型,我们也可以导入这个类型,并将其设置为这个对象。首先,在实用文件中,我们需要导出它,然后就可以像导入函数一样使用它,然后将它赋值给我们的对象。
import { Person, getGreeting } from "./utils";let person: Person = { firstName: 'bob', lastName: 'dylan'}let greeting = getGreeting(person);console.log(greeting);
总结
TypeScript可能比这更加复杂。但这就是要点。在大部分情况下,你定义自己的类型,TypeScript会确保你正确使用它们。
总结一下,使用TypeScript有三个主要原因:
- 你可以获取函数的类型信息
- 你知道它们返回什么
- 你知道它们对你的期望,即使不看函数本身
TypeScript确保你正确地连接应用程序。它强制你使用正确的参数调用函数,并迫使你考虑边界情况。
TypeScript在重构过程中帮助很多。它不会让你遗漏需要更改的代码部分,也不会放过拼写错误。
Leave a Reply