TypeScript 类型深入探究 - 第一部分

2025-05-28

TypeScript 类型深入探究 - 第一部分

本文最初发表于Barbarian Meets Coding

TypeScript 是 JavaScript 的一个更现代、更安全的版本,它席卷了 Web 开发领域。它是 JavaScript 的一个超集,添加了一些附加功能、语法糖和静态类型分析,旨在提高您的工作效率并扩展 JavaScript 项目。

还没有读过太多有关 TypeScript 的内容吗?

本系列文章假设您对 TypeScript 有一定的了解。如果您还没有深入研究过 TypeScript,我建议您先阅读这篇入门文章

TypeScript 于 2012 年首次发布,当时它确实为 JavaScript 带来了许多新功能。这些功能直到 ES2015 及更高版本才在 JavaScript 中可用。然而,如今 TypeScript 和 JavaScript 之间的功能差距正在缩小,TypeScript 最强大的价值主张仍然是其强大的类型系统及其相关的开发工具。这个类型系统兑现了 TypeScript 的承诺:可扩展的 JavaScript,并为您带来卓越的开发体验:

  • 当你做傻事时立即得到反馈
  • 强大的语句完成功能
  • 无缝语义代码导航
  • 智能重构和自动代码修复
  • 以及更多

在本系列文章中,我们将探讨 TypeScript 的综合类型系统,并学习如何利用它来构建非常健壮且可维护的 Web 应用程序。

类型注解

类型注解是 TypeScript 类型系统的核心。它们是你在编写代码时提供的额外信息,以便 TypeScript 能够更好地理解代码,从而提供更佳的开发体验。

假设你有一个将两个数字相加的函数:

const add = (a, b) => a + b;
Enter fullscreen mode Exit fullscreen mode

只有 TypeScript 不知道 neither 和anorb应该都是数字。所以我们可以稍微表达一下,用类型注解来注释这些参数:

const add = (a: number, b: number) => a + b;
Enter fullscreen mode Exit fullscreen mode

现在 TypeScript 已经知道, 和 都a只能b是数字。因此,如果我们出于某种原因决定编写以下代码:

add(1, 'banana');
Enter fullscreen mode Exit fullscreen mode

TypeScript 编译器,我们忠实的伙伴,会查看我们的代码并发疯(它期望数字,而我们却给了它一个水果,多么淘气)。

这其中最棒的部分是什么?最棒的是,我们会立即收到这个错误。而不是像某些粗心的用户在生产系统中运行这段代码那样,等上几个小时、几天甚至几周才会收到。不会!我们会在错误发生后的几毫秒内收到。太棒了!反馈循环很短。它们让一切都变得更好。就像培根一样,或者……培根。

基本类型

您想快速试验一下 TypeScript 类型系统吗?

无需安装 TypeScript,就能体验 TypeScript 及其类型系统的绝佳方法是直接使用typescriptlang.org 的 TypeScript 开发环境进行编程。直接进入开发环境,输入一些代码,然后将鼠标悬停在变量、函数和类上,就能找到它们的类型。

TypeScript 中的基本类型对应于 JavaScript 的原始类型:

number
boolean
string
Date
Array<T>
Object
Enter fullscreen mode Exit fullscreen mode

因此,如果您想在 TypeScript 中定义一个字符串,您可以输入以下内容:

let myName: string = "Jaime";
Enter fullscreen mode Exit fullscreen mode

因为 TypeScript 的目标是让你的生活更轻松,所以在这种情况下,它会足够智能地推断myName变量的类型,这样你就不需要显式地注释它了。这意味着这样就足够了:

let myName = "Jaime";    // Type string
Enter fullscreen mode Exit fullscreen mode

所以...

let myName = "Jaime";    // Type string
let myAge = 23;          // Yeah sure! Type number
Enter fullscreen mode Exit fullscreen mode

和:

let myName = "Jaime";    // Type string
let myAge = 23;          // Yeah sure! Type number
let isHandsome = true;   // Type boolean
let birth = new Date();  // Type Date
Enter fullscreen mode Exit fullscreen mode

TypeScript 总是会尽力理解你的代码。TypeScript 编译器有一种称为类型推断的机制,它会尝试解释你的代码并找出代码的类型,这样你就不需要手动输入所有内容了。

Let 与 Const

所以如果:

let myName = "Jaime";    // Type string
Enter fullscreen mode Exit fullscreen mode

下面的变量的类型是什么myName

const myName = "Jaime";    // Type ?
Enter fullscreen mode Exit fullscreen mode

是吗string?是吗const stringSTRING是别的吗?

如果你像我一样,从来没有考虑过这个难题,你可能会像我一样惊讶地发现类型是"Jaime"什么?!?):

const myName = "Jaime";    // Type "Jaime"
Enter fullscreen mode Exit fullscreen mode

如果我们将示例扩展到其他原始类型,我们会看到:

const myName = "Jaime";    // Type "Jaime"
const myAge = 23;          // Type 23
const isHandsome = true;   // Type true
const birth = new Date();  // Type Date
Enter fullscreen mode Exit fullscreen mode

这里发生了什么?const在 JavaScript 和 TypeScript 中,这意味着上述变量在声明时只能绑定一次。因此,TypeScript 可以假设这些变量永远不会改变,并尽可能地限制它们的类型。在上面的例子中,这意味着常量的类型myName将是字面量类型 "Jaime",的类型myAge将是23等等。

那么 Date 呢?为什么const完全不影响它的类型?原因是,由于 Date 随时可能更改,TypeScript 无法进一步限制其类型。这个日期可能是now ,right now ,但有人随时可能把它改成yesterday , tomorrow。我的天哪。

让我们仔细看看文字类型,它们是什么以及为什么它们有用。

文字类型

所以:

const myName = "Jaime";    // Type "Jaime"
Enter fullscreen mode Exit fullscreen mode

上面字符串的类型是"Jaime"其本身。这是什么意思呢?这意味着该变量的唯一有效值myName是字符串"Jaime",而不是其他值。这些就是我们所说的字面量类型,你可以像 TypeScript 中的任何其他类型注解一样使用它们:

const myName : "Jaime" = "Jaime";
Enter fullscreen mode Exit fullscreen mode

因此,如果我尝试变得非常聪明并写下以下内容:

const myName : "Jaime" = "John";
Enter fullscreen mode Exit fullscreen mode

TypeScript 将会介入并抛出编译器错误:

const myName : "Jaime" = "John";
// => 💥 Type '"John" is not assignable to type '"Jaime"'
Enter fullscreen mode Exit fullscreen mode

太棒了!那么它到底有什么用呢?我们马上就能看到。但为了给你一个真正好的例子,我首先需要教你 TypeScript 类型库中另一个很酷的功能:联合

工会

假设我们正在构建一个库,让你可以使用 SVG 创建漂亮的可视化效果。为了设置 SVG 元素的属性,最好有一个类似这样的函数:

function attr(element, attribute, value) {}
Enter fullscreen mode Exit fullscreen mode

每个属性的类型可以表示如下:

function attr(element: SVGCircleElement, 
              attribute: string, 
              value: string) {}
Enter fullscreen mode Exit fullscreen mode

你可以像这样使用这个函数:

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "r", 5);
Enter fullscreen mode Exit fullscreen mode

这可行但是...如果拼错了属性怎么办?

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "radius", 5); 
// => 💥 Doesn't work! There's no radius in SVGCircleElement
Enter fullscreen mode Exit fullscreen mode

它会在运行时崩溃。虽然它可能不会立即崩溃,但它不会像你预期的那样工作。但这不正是类型系统和 TypeScript 应该帮助你解决的问题吗?没错!更好的方法是利用 TypeScript 类型系统,并使用类型字面量来进一步限制可能的属性数量:

function attr(element: SVGCircleElement,
              attribute: "cx" | "cy" | "r",
              value: string) {}
Enter fullscreen mode Exit fullscreen mode

"cx" | "cy" | "r"一个**联合类型"cx",表示一个可以是或** 类型"cy""r"。您可以使用|联合类型运算符来构建联合类型。

太棒了!所以,如果我们现在犯了和刚才一样的错误,TypeScript 会立即伸出援手,并给我们一些反馈:

attr(myCircle, "cx", 10);
attr(myCircle, "cy", 10);
attr(myCircle, "radius", 5); 
// => 💥 Type '"radius"' not assignable to type "cx" | "cy" | "r"
// 🤔 Oh wait! So the radius attribute in a circle is actually called "r"!
Enter fullscreen mode Exit fullscreen mode

利用类型字面量,您可以将可用类型限制为仅有意义的类型,从而创建更健壮、更易于维护的应用程序。一旦我们犯了类似上述错误,TypeScript 就会立即提醒我们,让我们能够立即修复。不仅如此,通过将这些丰富的类型信息提供给 TypeScript,TypeScript 编译器将能够提供更高级的功能,例如语句补全,并在我们在编辑器中输入时提供合适的属性建议。

如果你以前做过 SVG 可视化,上面的函数可能看起来很熟悉。这是因为它很大程度上受到了d3.Selection.attr以下函数的启发:

d3.select("svg")
  .attr("width", 100)
  .attr("height", 200)
Enter fullscreen mode Exit fullscreen mode

在过去的一个项目中,我们遇到了几个类似的问题,最终我们围绕 d3 创建了样板代码以避免拼写错误。迁移到 TypeScript 后,我​​们再也没有遇到过同样的问题。我们可以依靠类型系统的表达能力自行解决这个问题。

// A possible (naive) type definition for d3Selection
interface d3Selection {
  attr(attribute: 'width' | 'height' | etc..., value: number);
}
Enter fullscreen mode Exit fullscreen mode

真的吗?我需要手动添加所有属性名称吗?

上面的示例旨在帮助您理解字面量类型的实用性以及如何创建类型联合。然而,它可能让您误以为需要手动输入 SVGElement 的所有属性。虽然您可以手动输入,但 TypeScript 拥有更多有趣的特性,可以让您更方便地处理类型并从现有类型中提取类型信息。在本系列的后续文章中,您将学习另一种使用泛型和 keyof 运算符来输入函数类型的方法。

类型别名

像我们之前那样定义的属性类型可能会造成混淆,并且重用起来很麻烦:

function attr(element: SVGCircleElement,
              attribute: "cx" | "cy" | "r",
              value: string) {}
Enter fullscreen mode Exit fullscreen mode

类型别名是描述类型的一种便捷的简写,类似于昵称,可用于为类型提供更具描述性的名称,并允许您在代码库中重复使用它。

因此,如果我们想要创建一个可以表示所有可用属性的类型,SVGElement那么就可以创建一个别名,如下所示:

type Attribute = "cx" | "cy" | "r" // etc...
Enter fullscreen mode Exit fullscreen mode

一旦定义,我们就可以重写attr函数签名:

function attr(element: SVGCircleElement,
              attribute: Attribute,
              value: string) {}
Enter fullscreen mode Exit fullscreen mode

数组、元组和对象

您可以使用以下符号在 TypeScript 中输入数组:

let numbers: number[] = [1, 2, 3];
Enter fullscreen mode Exit fullscreen mode

或者:

let numbers: Array<number> = [1, 2, 3];
Enter fullscreen mode Exit fullscreen mode

我喜欢前者,因为它涉及的输入更少。由于我们只是初始化一个变量,TypeScript 可以推断其类型,所以在这种情况下你可以删除类型注释:

// TypeScript can infer that the type 
// of numbers is number[]
let numbers = [1, 2, 3];

numbers.push('wat');
// 💥 Argument of type '"wat"' is not assignable to parameter of type 'number'.
numbers.push(4);
// ✅ Yes!
numbers.psuh(5);
// 💥 Property 'psuh' does not exist on type 'number[]'.(2339)
Enter fullscreen mode Exit fullscreen mode

TypeScript 对元组也提供了很好的支持。元组可以看作是由两个、三个(三元组)、四个(四元组)或更多元素组成的有限数组。当你需要对多个彼此之间存在某种关系的有限项进行建模时,元组会非常方便。

我们可以像这样定义一个由两个元素组成的元组:

let position: [number, number] = [0, 0];
Enter fullscreen mode Exit fullscreen mode

如果我们现在尝试访问 tuplet 边界之外的元素,TypeScript 将会来拯救我们:

let something = position[2];
// 💥 Tuple type '[number, number]' of length '2' has no element at index '2'.
Enter fullscreen mode Exit fullscreen mode

我们可以按照类似的方法来定义具有更多元素的元组:

let triplet: [number, number, number];
let quadruplet: [number, number, number, number];
let quintuplet: [number, number, number, number, number];
// etc...
Enter fullscreen mode Exit fullscreen mode

有时你会发现自己在 TypeScript 中使用对象。对象字面量的类型如下:

const position: {x:number, y:number} = {x: 0, y: 0};
Enter fullscreen mode Exit fullscreen mode

同样,在这种情况下,TypeScript 可以推断对象文字的类型,因此可以省略类型注释:

const position = {x: 0, y: 0};
Enter fullscreen mode Exit fullscreen mode

如果你大胆尝试访问对象类型中未定义的属性,TypeScript 会对你生气:

const position = {x: 0, y: 0};

console.log(position.cucumber);
// 💥 Property cucumber doesn't exist in type {x:number, y:number}
Enter fullscreen mode Exit fullscreen mode

也就是说,TypeScript 为您提供了最大拼写错误1​​保护

就像我们之前使用类型别名来以更具描述性和更少冗长的方式来引用 HTML 属性一样,我们可以对对象类型采取相同的方法:

type Position2D = {x: number, y: number};
const position: Position2D = {x: 0, y: 0};
Enter fullscreen mode Exit fullscreen mode

这也会导致更具体的错误消息:

console.log(position.cucumber);
// 💥 Property cucumber doesn't exist in type Position2D
Enter fullscreen mode Exit fullscreen mode

交叉路口

对于类型来说,并集运算符的行为类似于或,而交集|运算&的行为类似于

假设你有一个定义狗的类型,它具有以下能力bark

type Dog = {bark():void};
Enter fullscreen mode Exit fullscreen mode

另一种类型描述可以绘制的事物:

type CanBeDrawn = {brush:Brush, paint():void}; 
Enter fullscreen mode Exit fullscreen mode

我们可以将这两个概念合并成一个新类型,描述一只狗,可以使用&运算符绘制:

type DrawableDog = Dog & CanBeDrawn;
Enter fullscreen mode Exit fullscreen mode

交叉类型有何用处?它们允许我们在 TypeScript 中使用类型来建模混合宏 (mixin)特质 (trait) ,这两种模式在 JavaScript 应用程序中都很常见。混合宏是一种可复用的行为,可以临时应用于现有对象和类,并使用新功能进行扩展。该&运算符允许你创建由两种或多种其他类型组合而成的新类型,就像 JavaScript 中的混合宏一样。如果你对混合宏还不是很熟悉,我写了很多关于它们优缺点的文章:

并集 | 和交集 & 运算符在 TypeScript 中被称为类型运算符。就像 + 或 - 这样的运算符允许你对值执行运算一样,类型运算符在神秘的类型世界中执行运算。

总结

毫无疑问,TypeScript 的表达类型系统是该语言中最有趣的特性,也是它兑现编写可扩展 JavaScript 的承诺的原因。

使用类型注解,你可以向 TypeScript 编译器提供额外的类型信息,从而简化你的开发工作,帮助你构建更健壮、更易于维护的应用程序。遵循同样的理念,TypeScript 编译器会尽力从你的代码中推断出类型,而无需你对代码的每个部分进行显式注解。

您可以使用的类型注解种类繁多,从原始类型(例如numberstring)到数组、任意对象、元组、接口、类、字面量类型等等。您甚至可以定义类型别名来提供描述性名称,使类型更易于理解和重用。

一组特别有趣的类型是类型字面量。类型字面量将单个值表示为一种类型。它们非常有用,因为它们允许你非常精细地约束变量或 API 的类型。我们已经看到了一个示例,展示了如何利用字面量类型为 d3 可视化库提供更安全的 API。

|使用并集或交集等类型运算符,&您可以将一个类型转换为另一个类型。类型系统的这种表现力和可塑性,让您能够构建高度动态的面向对象设计模式,例如混合宏 (mixin)。

今天就到这里!希望你喜欢这篇文章,之后还会有更多 TypeScript 类型的精彩内容。祝你拥有美好的一天!


  1. 我拼错了。哈哈 。↩

文章来源:https://dev.to/vintharas/typescript-types-deep-dive-part-1-4edo
PREV
我为 Web 开发者开发了一款应用!Responsivize
NEXT
职业发展的最佳途径