以下是每个 React 开发人员需要了解的 TypeScript 知识 - 第 1 部分
如果你已经使用 React 一段时间了,你会注意到 JavaScript 的自由和狂野特性有时会对你不利(这并非 JS 的错😄),尤其是在团队合作的时候。你可能还不知道,但你需要 TypeScript,或者至少,你需要测试它。
让我说清楚,我喜欢 JavaScript 和它提供的自由,很长一段时间我都“反对” TypeScript。
所以我想一起踏上一段旅程,弄清楚 TypeScript 是否值得使用,或者 TS 是否只适合那些不懂如何正确编码的人(这曾经是我团队中的一个内部笑话!)。
本文背后的想法是介绍 TS 的基础知识并了解其好处,以便您可以决定是否需要这些好处,在第二部分中,我将介绍 TS 与 React 的具体细节。
资源
如果您愿意,您可以直接转到示例项目或源代码,这是一个非常简单的项目,用于测试 TypeScript 开发人员体验,无需 Webpack 或任何其他附加组件,只需将普通的 TypeScript 转换为 JavaScript。
我在本文中提供的其他资源是React和NextJS 的样板:
如果您喜欢编程游戏,请尝试PhaserJS,您可以使用 TypeScript 为浏览器制作游戏,这是一种学习 TS 的有趣方式。
另外,请务必查看TS 官方手册,其中包含大量有用的文档和示例。
为什么选择 ESLint、Prettier 和 Husky
在样板上,我使用 Airbnb 的 ESLint 规则、Prettier 的推荐规则和 Husky 的预先提交操作,这将非常有用,特别是在团队环境中,您需要每个人都遵循相同的代码风格,但您也可以作为单独的开发人员或学习者受益。
Airbnb 规则在某些方面可能很奇怪,但它们提供了很好的解释和示例,以便您可以决定该规则是否适合您,如果没有,您可以在.eslintrc
文件中禁用它。
我发现对于初级人员或刚开始使用 JS 或 TS 的人来说,这些规则非常有用,所以我建议你至少尝试将它们包含在一个项目中并检查结果 😉
什么是 TypeScript
TypeScript或 TS 是由 Microsoft 开发和维护的开源语言,TS 还:
- 多范式语言(如 JavaScript)。
- JavaScript 的替代品(更准确地说是超集)
- 允许使用静态类型
- 额外功能(泛型、接口、元组等,将在下面详细解释)
- 允许逐步采用*。
- 可以用于前端和后端开发(就像 JS 一样)
*您可以通过逐个更改文件将现有项目转变为 TS 项目,这并不是一个大的改变。
浏览器无法理解 TS 代码,必须将其转编译成 JS。JS 具有动态类型映射值,而 TS 具有静态类型,静态类型不易出错。
在 React 中,您已经使用Babel转编译了JS ,因此现在转编译代码不再是一个额外的不便。
为什么要费心处理 TS?
问题就在这里,如果你对 JS 很满意,而且一切都很好,那为什么还要用 TS 呢?前段时间,我说过,我们之间有一个关于 TS 之类的语言带有类型的内部笑话(顺便说一下,我当时在做 Java),如果你不知道如何正确编写代码,就需要类型。
TypeScript、Java 和其他一些语言都具有静态类型,它们会定义与变量关联的类型,并在编译时检查该类型。一旦将某个变量定义为字符串或布尔值,就无法更改其类型。
另一方面,JavaScript 具有动态类型,您可以将字符串分配给变量,然后将其转换为布尔值、数字或任何您想要的类型,该类型将在运行时动态分配。
但是当你在网上查看 TS 代码时,你会看到......
所以回到我团队的老笑话,是的,它确实是正确的,如果你确切地知道自己在做什么,你就不需要有人不断地告诉你这是一个字符串,而且只是一个字符串,如果在某个时候它变成了布尔值或其他东西......我知道我在做什么!
但事实是,我们并不完美,事情总是会发生:
- 匆忙工作。
- 今天过得很糟糕。
- 如果你在周五留下了一个想法,那么当你周一回来时,你对情况的了解就不会一样了。
- 在团队中工作,并不是每个人都有相同的水平和/或愿景。
出于同样的原因,我们使用 IDE、IDE 扩展、语法高亮和 lintern 来代替记事本应用。TypeScript 可以融入这些辅助工具中。
例子中的一些错误
让我们看一些公式中包含和不包含 TS 的基本示例:
拜托,我知道我在用什么
// App.js
import { MemoryRouter as Router } from 'react-router-dom'
import Routes from './routes'
export default function App() {
return (
<Router basename="/my-fancy-app">
<Routes />
</Router>
)
}
你发现上面的代码有什么不寻常的地方了吗?如果有,恭喜你。
这个文件在我的样板文件里放了很久了,虽然不是 bug,但……根本MemoryRouter
不需要basename
。发生这种情况是因为过去某个时候BrowserRouter
用到了它,而它实际上需要一个basename
属性。
使用 TS 时,您将收到通知,No overload matches this call
告知您该组件没有具有该属性的签名。
TypeScript 不仅可以作为静态类型,还可以帮助您更好地理解其他库的需求,我所说的其他库是指来自第三方或您的同事的组件和功能。
是的,我能听到答案,你必须正确地了解你正在使用的库,是的,你是对的,但假设项目中的每个人都知道每个“外部”库和版本的细微差别可能是一项艰巨的任务。
魔鬼的旗帜
let isVerified = false;
verifyAmount();
// isVerified = "false"
if (isVerified) proceedPayment();
我已经多次看到过这个错误,我没有确切的代码,而且每次都有不同的细微差别,但你可以明白这一点,你有一个布尔变量,它负责让某些代码运行或不运行,在某些时候,其他人或者你自己会遇到错误,将布尔值变成字符串,而非空字符串就是真值。
使用 TypeScript 时,您可能会遇到以下错误:The type 'string' is not assignable to the type 'boolean'
即使您当时没有运行应用程序,此错误也会在编译时发生,因此该错误进入生产环境的可能性非常小。
再次,我们可以应用与以前相同的规则,如果您正确编写代码,这种情况就不会发生,如果您遵循清洁代码的规则并小心您所做的事情,这种情况也可以避免,TypeScript 并不意味着让我们懒惰和混乱,但它可以是一个很好的盟友,因为语法突出显示可以帮助避免某些错误或检测未使用的变量。
我以为盒子里的猫还活着
const MONTH_SELECT_OPTIONS = MONTHS.map((month) => ({
label: getMonthName(month),
value: month,
}))
export default function PaymentDisplayer() {
const [currentMonthFilter, setCurrentMonthFilter] = useState(
MONTH_SELECT_OPTIONS[0]
)
const onChangeHandler = option => {
setCurrentMonthFilter(option.value)
}
return (
<select onChange={onChangeHandler}>
{MONTH_SELECT_OPTIONS.map(({ label, value }) => (
<option key="value" value={value}>
{label}
</option>
))}
</select>
)
}
改变状态的类型是很常见的(也许不推荐),有时是故意的,比如有一个isError
标志,然后突然将其从布尔值 false 更改为错误消息字符串(再次不推荐!),但在其他情况下,这是错误的,就像上面的例子一样。
最初编写此代码的人以为currentMonthFilter
他会存储 select 的实际选项,即HTMLOptionElement
带有标签和值的选项。后来,同一个人,或者可能是另一个开发人员,在另一天创建了它changeHandler
并设置了值,而不是完整的选项。
上面的例子是有效的,并且为了学习而进行了简化,但想象一下大规模的情况,特别是在那些将动作作为道具传递的组件中。
这里 TypeScript 可以通过两种方式帮助我们:
- 当尝试将 的类型更改为 时,静态类型将
currentMonthFilter
引发{label: string, value: number}
错误number
。 - 编写下一步调用服务以使用该过滤器检索付款的人员将通过IntelliSense知道他们将从州获得什么类型以及它是否与服务所需的类型相匹配。
因此 TypeScript 还允许我们从 IDE 中检查同行的第三方库和组件的不同功能、参数和文档。
通过这些示例(老实说,这些示例可能不太具有代表性),我们可以得出结论,TypeScript 试图在 React 环境中帮助我们:
- 保持类型一致并与静态类型一致
- 提供可用可能性的文档和IntelliSense
- 尽早发现错误
设置 TypeScript
在本文中,我们将使用全局安装,因为我认为最好首先在没有任何 Webpack、React 或任何其他变量的情况下单独研究 TypeScript,看看它是如何工作的以及它解决了哪些问题,但让我们看看如何在不同的环境中安装:
使用 CRA (Create-React-App) 安装
- 您可以使用 TS 的 CRA 模板
yarn create react-app my-app --template typescript
- 您可以使用资源部分提供的现成样板。
如果是现有项目,您可以使用以下命令,并将您的 js 文件转换为 ts/tsx 文件。
npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest
# or
yarn add -D typescript @types/node @types/react @types/react-dom @types/jest
使用 Nextjs 安装
- 如果您安装 TypeScript 作为依赖项,Nextjs 会
tsconfig
在您启动时为您创建一个文件。 - 如果您创建一个
tsconfig
文件,Nextjs 将在启动后提供将 TypeScript 安装到项目中的说明。 - 您可以使用资源部分提供的现成样板。
npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest
# or
yarn add -D typescript @types/node @types/react @types/react-dom @types/jest
全局安装
npm install -g typescript
#or
yarn install --global typescript
TypeScript 编译器(tsc)
一旦您在系统上安装了 TypeScript 或使用了上面提到的任何其他选项,您就可以使用 TypeScript 编译器,即tsc
命令。
让我们用最低配置测试一下编译器:
- 创建新的空文件夹
- 放置一个
index.html
具有基本 HTML5 结构的内容。 - 创建一个
index.ts
与 同级的空文件index.html
。 - 打开一个终端并输入
tsc --init
(假设您已经安装了全局 typescript)这将为您创建一个tsconfig.json
(我们将在下一节详细研究这个文件)。
您将得到如下结果:
- index.html
- index.ts
- tsconfig.json
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
</html>
现在您需要在 HTML 中包含 ts 文件,但是浏览器不理解 TypeScript,它们理解 JavaScript,因此您可以修改index.html
为:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script src="./index.js"></script>
</html>
打开一个新的终端并输入tsc
。您的index.ts
文件将被转换为index.js
浏览器可以读取的文件。
tsc
您无需在每次想要将 TS 文件编译为 JS 文件时都输入命令,而是可以使用 将 TypeScript 置于监视模式tsc -w
。
现在我的建议是,你同时打开 TS 和 JS 文件,并在index.ts
文件中输入常规 JS,然后测试输出结果。(在接下来的部分中,我们会经常用到这个方法来测试 TS 生成的内容)。
tsconfig.json
如果您正在关注本文,那么您已经使用创建了一些默认配置和一堆注释的tsc --init
命令创建了此文件,这些注释非常适合入门。tsconfig.json
让我们看一些可能对您入门有用的属性:
target
是我们将 TS 代码转换为的 JS 版本。根据您想要支持的浏览器,您可能需要设置一些旧版本。这也可以是一个很好的学习资源,您可以尝试使用不同的版本,看看会生成什么样的 JS 代码。module
定义模块将使用哪种语法,commonjs
这是默认用法require/module.exports
和现代 JS(ES6+)用法import/export
。*lib
在 React 和 Nextjs 样板中我使用此设置,您需要它来指定您将在项目中使用的其他库并检查其他类型,例如 DOM 相关。jsx
在 React 中,您至少需要将其设置preserve
为此模式,假设另一个工具将编译该部分(在本例中为 Babel),但 TSC 将进行类型检查。**outDir
编译后文件将被放置在哪里,例如在大多数 React 项目中它将被放置在一个build
文件夹中。rootDir
文件将被带到哪里进行编译,在大多数 React 项目中,这将是./src
strict
启用一组类型检查规则,从而对“正确”的内容进行更严格的检查。我建议在学习时先将其设置为 false,等到您足够自信时再启用它,并检查有哪些新的危险信号。但请记住,启用此选项后,您将能够充分发挥 TS 的潜力。此选项还会启用以下所有严格选项,您可以单独禁用它们。include
您想要包含进行编译的文件夹,例如src
文件夹exclude
您想要阻止被编译的文件夹,例如node_modules
文件夹。
*如果您想使用,import/export
您需要更改target
为 ES6 或更高版本,在示例项目中我们将使用此语法,因此请查看文章的其余部分。
**如果您希望 TSC 将您的 JSX 代码编译为常规 JS 代码,则可以将此属性设置为react
或使用此属性,在大多数情况下,我们将保留此属性,将文件作为常规 JSX 发送,Babel/Webpack 将完成其余工作。react-native
preserve
在本文的示例项目中,我们将从rootDir
中获取文件./src
并将其放置outDir
在public
文件夹中。
购物清单
示例项目是非常基础的东西,您可以在不同的部分插入不同的商品及其数量,然后在购物时将它们移除并检查接下来要购买什么。
这个示例项目背后的想法是习惯 TypeScript 和一般工作流程,因为一旦你进入 React 环境,Webpack 或任何其他捆绑器都会为你完成很多神奇的事情,所以我认为了解最基本的东西并随后享受捆绑器为我们所做的工作非常重要。
让我们看看我们可以利用 TS 的什么来获得更好、更不容易出错的代码库。
模块
如果要使用 ES6import/export
模块,则必须配置tsconfig
:
- 目标:es6 或更高版本
- 模块:es2015 或更高版本
并且在index.html
文件中添加模块类型:
<script type="module" src="app.js"></script>
但是,使用模块有两个缺点:
- 与旧版浏览器的兼容性较差。
- 生产中的文件将被拆分,因此您将对每个文件有多个请求(这可以通过使用像 Webpack 这样的捆绑器来解决)。
类型
在 JavaScript 中,类型是在运行时分配的,当解释器看到变量和值时,它会决定它的类型,所以我们可以这样做:
let job = "Warrior"; // string
let level = 75; // number
let isExpansionJob = false; // boolean
level = "iLevel" + 75
// now it's an string
在 TypeScript 中,类型是在编译时分配的,因此一旦定义了类型,它将受到该签名的保护。
let job: string = "Samurai";
let level: number = 75;
let isExpansionJob: boolean = true;
level = "iLevel" + 75
// Error, Type string cannot
// be assign to type number!
推理
事实上,没有必要明确说明变量的类型,TS 可以根据它们的值推断类型。
let job = "Samurai";
let level = 75;
let isExpansionJob = true;
level = "iLevel" + 75
// Error, Type string cannot
// be assign to type number!
在 React 中,我们将在本文的第 2 部分中详细介绍,你也会看到推论,例如useState
const [currentMonthFilter, setCurrentMonthFilter] = useState("January")
useEffect(() => {
setCurrentMonthFilter(1)
// Error, Type number cannot
// be assign to type string!
}, [])
任意和未知
我一直说 TS 具有静态类型,但该说法有一个细微差别。
let level: any = 10;
level = "iLevel" + 125;
// OK, still type any
level = false;
// OK, still type any
欢迎回到 JavaScript!any
当您不知道变量将来会是什么类型时,它是一种动态类型,但它以某种方式逆转了 TS 提供的所有优势。
let level: any = 10;
level = "iLevel" + 125;
level = false;
let stringLevel: string = level;
console.log(typeof stringLevel);
stringLevel.replace("false", "true");
当您分配level
给stringLevel
类型时string
,它不会变成字符串,它仍然是布尔值,因此该replace
函数不存在并且代码在运行时失败。Uncaught TypeError: stringLevel.replace is not a function
为此,我们有另一种类型,它是该any
类型的安全对应物:
let level: unknown = 10;
level = "iLevel" + 125;
level = false;
let stringLevel: string = level;
// Error
你unknown
可以像 中一样分配任意类型,any
但这次当你尝试分配其他类型时,编译器会报错。所以,如果你不知道具体类型,可以尝试使用 unknown 而不是 any。
数组
let job = "Red Mage";
let level = 75;
let isExpansionJob = false;
let jobAbilities = ['Chainspell', 'Convert'];
jobAbilities.push('Composure'); // OK
jobAbilities.push(2); // Error
jobAbilities[0] = 2; // Error
在上面的例子中,我们声明了一个字符串数组jobAbilities
,我们可以添加更多的字符串,但是我们不能添加其他类型或者将当前值更改为其他类型的值,因为在声明中我们已经做出了类型的推断string[]
。
let job = "Red Mage";
let level = 75;
let isExpansionJob = false;
let jobAbilities = ['Chainspell', 'Convert'];
let swordSkill = ["B", 5, 144, 398];
swordSkill.push("B+"); // OK
swordSkill.push(230); // OK
swordSkill[1] = "C";
// OK, the type is not position related
swordSkill.push(true); // Error
与前面的示例一样,类型推断是在声明中完成的,我们现在为声明一个字符串和数字的数组swordSkill
。
如果您想明确声明我们在示例中看到的数组的类型:
let jobAbilities: string[] = ['Chainspell', 'Convert'];
let swordSkill: (string | number)[] = ["B", 5, 144, 398];
顺便说一下,|
是为了做union
不同类型的事情。
对象
让我们回到示例,但现在以对象的形式:
let job = {
name: "Summoner",
level: 75,
isExpansion: true,
jobAbilities: ["Astral Flow", "Elemental Siphon"]
};
job.name = "Blue Mage"; // OK
job.level = "Four" // Error
job.avatars = ["Carbuncle"]; // Error
job.level = "Four"
无法做到这一点,因为我们无法改变属性的类型,属性也有静态类型。job.avatars = ["Carbuncle"]
我们无法添加新属性,该job
对象已经具有已定义结构的类型。
let job = {
name: "Summoner",
level: 75,
isExpansion: true,
jobAbilities: ["Astral Flow", "Elemental Siphon"]
};
job = {
name: "Blue Mage",
level: 4,
isExpansion: true,
jobAbilities: ["Azure Lore", "Burst Affinity"]
}; // OK
job = {
name: "Corsair",
level: 25,
isExpansion: true
}; // Error
我们可以分配另一个对象,因为我们将该对象定义为,let
但它必须具有完全相同的形式。
想一想,有多少次你在前端重复了对象结构而没有进行任何类似的检查?有多少次你打字时打错了data.descrption
,几天后才发现这个 bug?如果没有,我可以向你保证,这种情况很快就会发生。
让我们检查一下示例的明确类型:
let job: {
name: string;
level: number;
isExpansion: boolean;
jobAbilities: string[];
} = {
name: "Summoner",
level: 75,
isExpansion: true,
jobAbilities: ["Astral Flow", "Elemental Siphon"]
};
正如您所看到的,对于一个简单的对象来说,它会变得更大一些,所以在这种情况下我们可以使用type aliases
。
别名
type Job = {
name: string;
level: number;
isExpansion: boolean;
jobAbilities: string[];
};
let Summoner: Job = {
name: "Summoner",
level: 75,
isExpansion: true,
jobAbilities: ["Astral Flow", "Elemental Siphon"]
};
let BlueMage: Job = {
name: "Blue Mage",
level: 4,
isExpansion: true,
jobAbilities: ["Azure Lore", "Burst Affinity"]
};
使用类型别名,我们可以定义一个通用类型以供复用。在 React、DOM 和其他库中,你会发现许多现成的已定义类型。
功能
函数的语法与 JS 非常相似,但您可以指定参数的类型和返回的类型。
type Enemy = {
name: string;
hp: number;
level: number;
exp: number;
};
let attack = (target: Enemy) => {
console.log(`Attacking to ${target.name}`);
};
attack = "Hello Enemy"; // Error
我使用了箭头函数,但你也可以使用普通的函数声明。JS 和 TS 的函数有两点不同:
- 您指定传递给函数的参数的类型,例如我们的
target: Enemy
。 - 该变量
attack
被赋予了函数返回的类型,因此您之后无法更改其类型。
该函数的类型描述如下:
let attack = (target: Enemy): void => {
console.log(`Attacking to ${target.name}`);
};
当返回类型为无时使用该void
类型,并且也不需要明确设置类型:
// let attack = (target: Enemy): number => {
let attack = (target: Enemy) => {
return target.hp - 2;
};
与any
类型void
有一些细微差别:
let attack = (target: Enemy): void => {
console.log(`Attacking to ${target.name}`);
};
attack = (target: Enemy): number => {
return target.hp - 2;
};
// lizard has 200hp
console.log(attack(lizard)); // 198
上面的例子没有错误,即使您认为已经从 更改attack
为(target: Enemy) => void
它(target: Enemy) => number
仍然是void
。
检查如果使用第一个定义函数会发生什么number
。
let attack = (target: Enemy) => {
return target.hp - 2;
};
attack = (target: Enemy) => {
console.log(`Attacking to ${target.name}`);
}; // Error
let attackResult = attack(lizard);
Type '(target: Enemy) => void' is not assignable to the type '(target: Enemy) => number'
。。Type 'void' is not assignable to the type 'number'
所以,就像在这种情况下void
一样工作any
。
对于的attackResult
类型将是number
,不需要指定它,TS 将从函数的返回类型推断类型。
可选参数
可选参数可以在函数中定义?
let heal = (target: Player | Enemy, spell: Spell, message?: string) => {
if (message) console.log(message);
return target.hp + spell.power;
};
heal(player1); // Error
heal(player1, cure, "Healing player1"); // OK
heal(skeleton, cure); // OK
第一次调用将无法工作,因为我们需要传递至少两个参数,但第二次和第三次调用是可以的,message
是可选参数,当不传递时它将被接收为undefined
。
如果将最后一个例子与一个简单的 JS 函数进行比较:
let heal = (target, spell, message) => {
if (message) console.log(message);
return target.hp + spell.power;
};
heal(player1); // Error
heal(player1, cure, "Healing player1"); // OK
heal(skeleton, cure); // OK
基本行为相同,但不同之处在于错误将在运行时出现,因为在第一次调用中您不能power
从未定义的值调用。
从这些示例中可以看出,在 TS 中使用函数更安全,因为您无需依赖外部发生的情况,只需知道必须传入哪些参数以及它们的形式。对于使用您函数的用户来说也是如此,他们会确切地知道需要哪些参数、参数的形式以及他们将从函数中获取什么。
枚举
使用枚举我们可以定义常量的集合。
enum BattleMenu {
ATTACK,
MAGIC,
ABILITIES,
ITEMS,
DISENGAGE
}
enum Equipment {
WEAPON = 0,
HEAD = 1,
BODY = 2,
HANDS = 3,
LEGS = 4
}
console.log(BattleMenu.ATTACK, Equipment.WEAPON);
// 0 0
枚举默认是自动索引的,上面示例中的两个语句是等效的。
枚举也可以存储字符串,例如在 React 中我经常使用枚举来存储路径:
enum Routes {
HOME = "/",
ABOUT = "/about",
BLOG = "/blog"
}
泛型
const getPartyLeader = (memberList: Player[]) => {
return memberList[0];
};
const partyLeader = getPartyLeader(partyA);
我们想要实现一个getPartyLeader
函数,返回数组中第一个党派领导者组。
如果我们想支持除 之外的其他类型怎么办Player
?目前我们可以想到以下解决方案:
const getPartyLeader = (memberList: Player[] | Enemy[]) => {
return memberList[0];
};
const partyLeader = getPartyLeader(partyA);
// Player[] | Enemy[]
好的,现在我们可以传递一个Player
组或一个Enemy
组,但我们的PartyLeader
常量可以是任一,所以类型检查是Player[] | Enemy[]
。
如果我们想精确分配类型,一种方法是使用泛型:
const getPartyLeader = <T>(memberList: T[]) => {
return memberList[0];
};
const partyLeader = getPartyLeader(partyA); // Player
由于partyA
充满了Player
类型,partyLeader
因此其类型为Player
。我们先来检查一下语法:
T
是定义泛型的常用方法,但您可以随意调用它。
现在的问题可能是,由于any
T 接受所有内容,因此我们可以调整想要传递给该函数的内容类型:
type Player = {
name: string;
hp: number;
};
type Enemy = {
name: string;
hp: number;
};
type Spell = {
name: string;
power: number;
};
const getPartyLeader = <T extends { hp: number }>(memberList: T[]) => {
return memberList[0];
};
const playerPartyLeader = getPartyLeader(partyOfPlayers); // Ok
const enemyPartyLeader = getPartyLeader(partyOfEnemies); // Ok
const whatAreYouTrying = getPartyLeader(spellList); // Error
我们现在只能传递包含hp
属性的类型。
元组
正如我们之前看到的,数组可以包含不同的类型,但不受位置的限制,元组类型只是为了解决这个问题:
type Weapon = {
name: string;
damage: number;
};
type Shield = {
name: string;
def: number;
};
const sword: Weapon = {
name: "Onion Sword",
damage: 10
};
const shield: Shield = {
name: "Rusty Shield",
def: 5
};
let equipment: [Weapon, Shield, boolean];
equipment = [sword, shield, true]; // OK
equipment[2] = false; // OK
equipment = [shield, sword, false]; // Error
equipment[1] = true; // Error
我们现在有一个类似数组的类型,它关心类型的放置位置。
课程
随着 ES6 类被添加到 JavaScript,因此 JS 类和 TS 类之间没有太大区别。
class Job {
public name: string;
private level: number;
readonly isExpansion: boolean;
constructor(name: string, level: number, isExpansion: boolean) {
this.name = name;
this.level = level;
this.isExpansion = isExpansion;
}
}
const whiteMage = new Job("White Mage", 75, false);
console.log(whiteMage.name); // "White Mage"
console.log(whiteMage.level); // Error
console.log(whiteMage.isExpansion); // false
whiteMage.name = "Blue Mage"; // Ok
whiteMage.level = 50; // Error
whiteMage.isExpansion = true; // Error
在 TS 类中,您可以使用类的属性的访问修饰符:
- public - 属性和方法可从所有位置访问,这是默认值。
- private – 您只能访问同一个类内的属性。
- protected - 限制对类和子类的访问。
- readonly - 将属性标记为不可变。
接口
与我们看到类似type aliases
,我们可以通过 定义类型interface
。
interface Enemy {
name: string;
hp: number;
}
let attack = (target: Enemy): void => {
console.log(`Attacking to ${target.name}`);
};
那么,它看起来和 一样type aliases
,对吧?那么该用哪一个呢?随着 TS 的不同版本,两者的功能都在不断增强,现在它们之间的细微差别已经非常小了。我喜欢遵循这篇文章中的经验法则,它详细解释了 和 的区别:
如果您编写面向对象的代码 - 请使用接口,如果您编写功能代码 - 请使用类型别名。
所以在 React 中我们更习惯于编写函数式代码所以使用type aliases
。
DOM 操作
在 React 中我们不会(直接)使用太多 DOM 操作,但我认为了解其工作原理很有用。
从 DOM 中检索元素
// HTMLFormElement | null
const form = document.querySelector("form");
// HTMLElement | null
const otherForm = document.getElementById("myFancyForm");
// HTMLSelectElement
const select = document.createElement("select");
当我们执行时,document.querySelector("form")
我们的常量form
是用类型HTMLFormElement
或推断null
的,但在第二个例子中,我们通过它的 ID 获取一个表单,而 TS 不知道它到底是什么 HTML 元素,所以它给出了一个更通用的类型HTMLElement
。
const form = document.querySelector("form");
form.addEventListener("submit", (e: Event) => {
e.preventDefault();
console.log(e);
}); // Error
TS 不知道它能否在 HTML 中找到关于查询选择器的任何信息,因此无法将该addEventListener
函数赋值给可能的 null 类型。您可以通过三种方式解决这个问题。
我向你保证,你将会找到这个元素:
// HTMLFormElement
const form = document.querySelector("form")!;
你!
告诉 TS 不要担心,他会找到的,这不可能null
。
仅当它不为空时才执行此操作:
const form = document.querySelector("form");
form?.addEventListener("submit", (e: Event) => {
e.preventDefault();
console.log(e);
});
你可能已经见过?
JS可选链式运算符
现在是类型转换时间:
const otherForm = document.getElementById("myFancyForm") as HTMLFormElement;
otherForm.addEventListener("submit", (e: Event) => {
e.preventDefault();
console.log(e);
});
您告诉 TS 当它找到该元素时将获得什么类型,通过这个,您可以确保它是HTMLFormElement
而不是null
。
结论
正如我们所见,TypeScript 添加了许多额外的语法,这些语法在最终代码中根本不存在,但这些额外的努力使我们能够始终证明我们的数据结构决策是合理的,并在整个应用程序中保持一致。
当然,使用 TypeScript 会更耗时,尤其是在开始的时候,但对于那些需要大量更新、需求变化或最重要的员工流动的项目中,它可以成为救星。
编码不仅仅是制作一个高效的算法,你还需要与其他人一起工作(即使你在某些时候作为一名单独的开发人员,你也可能会发布你的作品,寻求合作或帮助),在这种情况下,成员之间的良好沟通是关键。
我喜欢将 TypeScript 视为人类的 Babel,您可以使用 Babel 针对 CPU 优化代码,但您需要一些东西来扩展并引导其他人理解您的想法,反之亦然。
只剩下一个问题:何时使用 TypeScript?
- 如果您与更多人合作或计划发布您的代码,您很可能希望代码尽可能具有可读性并能代表您的想法。
- 如果您正在进行一个大型项目。*
*每个大项目都是从一个小项目开始的,因此请谨慎使用此声明,仅将其用于“大”项目。
毫无疑问,这篇文章很长,如果您读到这里,我必须非常感谢您的努力和热情。我最初的想法并没有那么宏大,但我想解释一下其中的原因。希望您喜欢这篇文章。如果您已经从 JS 切换到 TS,正在同时使用两者,正在考虑,曾经考虑过但不喜欢,或者有其他情况,我都想听听您的经验。
文章来源:https://dev.to/dastasoft/here-s-what-every-react-developer-needs-to-know-about-typescript-part-1-48ob