日常类型
在本章中,我们将介绍 JavaScript 代码中最常见的一些值类型,并解释在 TypeScript 中描述这些类型的相应方法。 这并非一份详尽的列表,后续章节将介绍更多命名和使用其他类型的方式。
类型不仅出现在类型注解中,还可以出现在更多地方。 当我们学习这些类型本身时,我们也会了解可以在哪些地方引用这些类型来形成新的结构。
我们将从回顾编写 JavaScript 或 TypeScript 代码时可能遇到的最基本和最常见的类型开始。 这些类型将构成更复杂类型的核心基础模块。
原始类型:string、number 和 boolean
JavaScript 有三种非常常用的原始类型:string、number 和 boolean。 它们在 TypeScript 中都有对应的类型。 正如您所料,这些名称与您在这些类型的值上使用 JavaScript typeof 运算符时看到的名称相同:
string表示字符串值,如"Hello, world"number表示数字,如42。JavaScript 没有整数运行时值,因此没有与int或float对应的类型 —— 一切都只是numberboolean表示两个值true和false
类型名称
String、Number和Boolean(首字母大写)是合法的,但它们指的是一些特殊的內建类型,这些类型在你的代码中很少出现。始终使用string、number或boolean作为类型。
数组
要指定像 [1, 2, 3] 这样的数组的类型,可以使用语法 number[];此语法适用于任何类型(例如 string[] 表示字符串数组,依此类推)。 您也可能会看到 Array<number> 这种写法,意思是一样的。 当我们介绍泛型时,我们将更多地了解 T<U> 语法。
注意
[number]是不同的事物;请参考元组部分。
any
TypeScript 还有一种特殊类型 any,当您不希望某个特定值导致类型检查错误时,可以使用它。
当一个值是 any 类型时,您可以访问它的任何属性(这些属性也将是 any 类型),像函数一样调用它,将其赋值给任何类型的值(或从任何类型的值赋值给它),或者几乎任何在语法上合法的操作:
let obj: any = { x: 0 };
// 以下代码行都不会抛出编译器错误。
// 使用 `any` 会禁用所有进一步的类型检查,并且假定
// 您比 TypeScript 更了解环境。
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;Try当您不想编写一个长类型仅仅为了让 TypeScript 相信某行代码没问题时,any 类型非常有用。
noImplicitAny
当您没有指定类型,并且 TypeScript 无法从上下文中推断出类型时,编译器通常会默认为 any。
但是,您通常希望避免这种情况,因为 any 是不进行类型检查的。 使用编译器选项 noImplicitAny 将任何隐式 any 标记为错误。
变量的类型注解
当您使用 const、var 或 let 声明变量时,可以选择添加类型注解来显式指定变量的类型:
TypeScript 不使用像
int x = 0;这样的“左侧类型”风格声明。 类型注解总是放在被注解的事物之后。
然而,在大多数情况下,这并非必需。 只要可能,TypeScript 会尝试自动推断您代码中的类型。 例如,变量的类型是根据其初始化器的类型推断的:
在大多数情况下,您不需要显式学习推断规则。 如果您是初学者,请尝试使用比您认为更少的类型注解——您可能会惊讶于 TypeScript 完全理解情况所需的注解如此之少。
函数
函数是在 JavaScript 中传递数据的主要手段。 TypeScript 允许您指定函数的输入和输出值的类型。
参数类型注解
当您声明一个函数时,可以在每个参数后添加类型注解,以声明该函数接受什么类型的参数。 参数类型注解放在参数名称之后:
当参数具有类型注解时,将检查该函数的实参:
即使您的参数没有类型注解,TypeScript 仍会检查您传递的参数数量是否正确。
返回类型注解
您还可以添加返回类型注解。 返回类型注解出现在参数列表之后:
与变量类型注解非常相似,您通常不需要返回类型注解,因为 TypeScript 会根据其 return 语句推断函数的返回类型。 上面示例中的类型注解不会改变任何东西。 有些代码库会出于文档目的、防止意外更改或仅仅因为个人偏好而显式指定返回类型。
返回 Promise 的函数
如果您想注解一个返回 Promise 的函数的返回类型,您应该使用 Promise 类型:
匿名函数
匿名函数与函数声明略有不同。 当一个函数出现在 TypeScript 可以确定它将被如何调用的地方时,该函数的参数会自动获得类型。
这是一个例子:
const names = ["Alice", "Bob", "Eve"];
// 函数的上下文类型化 - 参数 s 被推断为 string 类型
names.forEach(function (s) {
console.log(s.toUpperCase());
});
// 上下文类型化也适用于箭头函数
names.forEach((s) => {
console.log(s.toUpperCase());
});Try尽管参数 s 没有类型注解,但 TypeScript 使用了 forEach 函数的类型以及数组的推断类型来确定 s 将具有的类型。
这个过程称为上下文类型化,因为函数出现的上下文提示了它应该具有什么类型。
与推断规则类似,您不需要显式学习这是如何发生的,但理解它确实会发生可以帮助您注意到何时不需要类型注解。 稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。
对象类型
除了原始类型之外,您会遇到的最常见的类型是对象类型。 这指的是任何具有属性的 JavaScript 值,这几乎涵盖了所有值! 要定义对象类型,我们只需列出其属性及其类型。
例如,这里有一个函数接受一个类似点的对象:
// 参数的类型注解是一个对象类型
function printCoord(pt: { x: number; y: number }) {
console.log("坐标的 x 值是 " + pt.x);
console.log("坐标的 y 值是 " + pt.y);
}
printCoord({ x: 3, y: 7 });Try这里,我们用具有两个属性——x 和 y——的类型注解了参数,这两个属性都是 number 类型。 您可以使用 , 或 ; 来分隔属性,最后一个分隔符是可选的。
每个属性的类型部分也是可选的。 如果您不指定类型,它将被假定为 any。
可选属性
对象类型还可以指定它们的一些或所有属性是可选的。 要做到这一点,请在属性名称后添加一个 ?:
function printName(obj: { first: string; last?: string }) {
// ...
}
// 两者都可以
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });Try在 JavaScript 中,如果您访问一个不存在的属性,您将得到值 undefined 而不是运行时错误。 因此,当您读取一个可选属性时,您必须在使用它之前检查 undefined。
function printName(obj: { first: string; last?: string }) {
// 错误 - 如果没有提供 'obj.last' 可能会崩溃!
console.log(obj.last.toUpperCase()); if (obj.last !== undefined) {
// 没问题
console.log(obj.last.toUpperCase());
}
// 使用现代 JavaScript 语法的一种安全替代方法:
console.log(obj.last?.toUpperCase());
}Try联合类型
TypeScript 的类型系统允许您使用各种运算符从现有类型构建新类型。 既然我们已经知道如何编写几种类型,是时候开始以有趣的方式组合它们了。
定义联合类型
您可能看到的第一种组合类型的方式是联合类型。 联合类型是由两种或多种其他类型组成的类型,表示值可能是这些类型中的任何一种。 我们将这些类型中的每一种称为联合的成员。
让我们编写一个可以对字符串或数字进行操作的函数:
function printId(id: number | string) {
console.log("您的 ID 是:" + id);
}
// 没问题
printId(101);
// 没问题
printId("202");
// 错误
printId({ myID: 22342 });Try联合成员的分离符允许放在第一个元素之前,所以您也可以这样写:
tsTryfunctionprintTextOrNumberOrBool(textOrNumberOrBool: | string | number | boolean ) {console.log(textOrNumberOrBool); }
使用联合类型
提供一个匹配联合类型的值很容易——只需提供匹配联合任何成员的类型即可。 但是如果您有一个联合类型的值,您该如何使用它呢?
TypeScript 只允许操作对于联合的每个成员都是有效的。 例如,如果您有联合 string | number,您不能使用仅在 string 上可用的方法:
解决方案是用代码缩小联合,就像您在 JavaScript 中没有类型注解时做的那样。 缩小发生在 TypeScript 可以根据代码结构推断出值的更具体类型时。
例如,TypeScript 知道只有 string 值会有 typeof 值 "string":
function printId(id: number | string) {
if (typeof id === "string") {
// 在此分支中,id 的类型是 'string'
console.log(id.toUpperCase());
} else {
// 在这里,id 的类型是 'number'
console.log(id);
}
}Try另一个例子是使用像 Array.isArray 这样的函数:
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// 在这里:'x' 是 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// 在这里:'x' 是 'string'
console.log("Welcome lone traveler " + x);
}
}Try注意在 else 分支中,我们不需要做任何特殊的事情——如果 x 不是 string[],那么它一定是 string。
有时您会有一个联合,其中所有成员都有共同点。 例如,数组和字符串都有 slice 方法。 如果联合中的每个成员都有一个共同的属性,您可以使用该属性而无需缩小:
// 返回类型被推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}Try类型的联合似乎具有这些类型的交集属性,这可能令人困惑。 这不是偶然——名称联合来源于类型理论。 联合
number | string是通过取每个类型值的并集组成的。 注意到给定两个集合以及关于每个集合的相应事实,只有这些事实的交集适用于这些集合本身的并集。 例如,如果我们有一个戴帽子的高个子房间,另一个戴帽子的西班牙语使用者房间,在合并这些房间后,我们知道的关于每个人的唯一事情是他们一定戴着帽子。
类型别名
我们一直通过直接在类型注解中编写对象类型和联合类型来使用它们。 这很方便,但经常希望多次使用同一个类型并用一个名称引用它。
类型别名正是如此——任何类型的名称。 类型别名的语法是:
type Point = {
x: number;
y: number;
};
// 与之前的示例完全相同
function printCoord(pt: Point) {
console.log("坐标的 x 值是 " + pt.x);
console.log("坐标的 y 值是 " + pt.y);
}
printCoord({ x: 100, y: 100 });Try实际上,您可以使用类型别名给任何类型起一个名字,而不仅仅是对象类型。 例如,类型别名可以命名联合类型:
请注意,别名仅仅是别名——您不能使用类型别名来创建同一类型的不同/独特的“版本”。 当您使用别名时,就像您编写了被别名的类型一样。 换句话说,这段代码看起来可能不合法,但根据 TypeScript 是没问题的,因为这两种类型都是同一类型的别名:
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
// 创建一个已清理的输入
let userInput = sanitizeInput(getInput());
// 但仍然可以用字符串重新赋值
userInput = "new input";Try接口
接口声明是命名对象类型的另一种方式:
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("坐标的 x 值是 " + pt.x);
console.log("坐标的 y 值是 " + pt.y);
}
printCoord({ x: 100, y: 100 });Try就像我们上面使用类型别名一样,这个示例就像我们使用了匿名对象类型一样有效。 TypeScript 只关心我们传递给 printCoord 的值的结构——它只关心它是否具有预期的属性。 只关心类型的结构和能力是我们称 TypeScript 为结构类型类型系统的原因。
类型别名与接口的区别
类型别名和接口非常相似,在许多情况下您可以在它们之间自由选择。 interface 的几乎所有特性在 type 中都是可用的,关键区别在于类型不能重新打开以添加新属性,而接口始终是可扩展的。
接口 | 类型 |
|---|---|
扩展接口 | 通过交叉扩展类型 |
向现有接口添加新字段 | 类型创建后不能更改 |
您将在后续章节中了解更多关于这些概念的内容,所以如果您现在不完全理解所有这些,请不要担心。
- 在 TypeScript 4.2 版本之前,类型别名名称可能出现在错误信息中,有时会替代等效的匿名类型(这可能是可取的,也可能不是)。接口将始终在错误信息中被命名。
- 类型别名可能不参与声明合并,但接口可以。
- 接口只能用于声明对象的形状,不能重命名原始类型。
- 接口名称在错误信息中始终以其原始形式出现,但仅当它们按名称使用时。
- 使用
extends的接口通常比使用交叉类型的类型别名对编译器来说性能更好
在大多数情况下,您可以根据个人喜好选择,如果 TypeScript 需要某种声明类型,它会告诉您。如果您想要一个经验法则,请使用 interface,直到您需要使用 type 的特性。
类型断言
有时您会拥有关于值类型的信息,而 TypeScript 无法知道。
例如,如果您使用 document.getElementById,TypeScript 只知道这将返回某种 HTMLElement,但您可能知道您的页面将始终具有给定 ID 的 HTMLCanvasElement。
在这种情况下,您可以使用类型断言来指定更具体的类型:
像类型注解一样,类型断言会被编译器移除,不会影响代码的运行时行为。
您也可以使用尖括号语法(除非代码在 .tsx 文件中),这与上述等价:
提醒:因为类型断言在编译时被移除,所以没有与类型断言关联的运行时检查。 如果类型断言错误,不会产生异常或
null。
TypeScript 只允许类型断言转换为类型的更具体或更不具体的版本。 此规则防止“不可能”的强制转换,例如:
有时此规则可能过于保守,并且会禁止可能有效的更复杂的强制转换。 如果发生这种情况,您可以使用两个断言,首先转换为 any(或 unknown,我们稍后会介绍),然后转换为所需的类型:
字面量类型
除了通用类型 string 和 number,我们还可以在类型位置引用特定的字符串和数字。
一种思考方式是考虑 JavaScript 提供了不同的声明变量的方式。var 和 let 都允许更改变量内部的内容,而 const 则不允许。这反映在 TypeScript 如何为字面量创建类型上。
let changingString = "Hello World";
changingString = "Olá Mundo";
// 因为 `changingString` 可以代表任何可能的字符串,这就是
// TypeScript 在类型系统中描述它的方式
changingString;
const constantString = "Hello World";
// 因为 `constantString` 只能代表 1 种可能的字符串,所以它
// 具有字面量类型表示
constantString;
Try单独使用字面量类型本身并不是很有价值:
让一个变量只能有一个值并没有太大用处!
但是通过将字面量组合成联合,您可以表达一个更有用的概念——例如,只接受一组特定已知值的函数:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");Try数字字面量类型的工作方式相同:
当然,您可以将这些与非字面量类型组合:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");Try还有一种字面量类型:布尔字面量类型。 只有两种布尔字面量类型,正如您所猜想的,它们是类型 true 和 false。 类型 boolean 本身实际上只是联合 true | false 的别名。
字面量推断
当您使用对象初始化变量时,TypeScript 假定该对象的属性以后可能会更改值。 例如,如果您编写了如下代码:
TypeScript 不认为将 1 赋值给先前具有 0 的字段是错误的。 另一种说法是 obj.counter 必须具有类型 number,而不是 0,因为类型用于确定读取和写入行为。
字符串也是如此:
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);Try在上面的例子中,req.method 被推断为 string,而不是 "GET"。因为代码可以在创建 req 和调用 handleRequest 之间进行评估,这可能会将像 "GUESS" 这样的新字符串赋值给 req.method,所以 TypeScript 认为此代码有错误。
有两种方法可以解决这个问题。
您可以通过在任一位置添加类型断言来改变推断:
ts
Try// 更改 1: constreq= {url: "https://example.com",method: "GET" as "GET" }; // 更改 2handleRequest(req.url,req.methodas "GET");更改 1 意味着“我打算让
req.method始终具有字面量类型"GET"”,防止之后将"GUESS"赋值给该字段。 更改 2 意味着“我出于其他原因知道req.method的值为"GET"”。您可以使用
as const将整个对象转换为类型字面量:ts
Tryconstreq= {url: "https://example.com",method: "GET" } asconst;handleRequest(req.url,req.method);
as const 后缀的作用类似于 const,但针对的是类型系统,确保所有属性都被赋予字面量类型,而不是像 string 或 number 这样的更通用版本。
null 和 undefined
JavaScript 有两个用于表示缺失或未初始化值的原始值:null 和 undefined。
TypeScript 有两个对应的同名类型。这些类型的行为取决于您是否开启了 strictNullChecks 选项。
strictNullChecks 关闭
在 strictNullChecks 关闭的情况下,可能是 null 或 undefined 的值仍然可以正常访问,并且值 null 和 undefined 可以赋值给任何类型的属性。 这类似于没有空值检查的语言(例如 C#、Java)的行为。 缺乏对这些值的检查往往是错误的主要来源;如果在其代码库中可行,我们始终建议人们开启 strictNullChecks。
strictNullChecks 开启
在 strictNullChecks 开启的情况下,当一个值是 null 或 undefined 时,您需要在使用该值上的方法或属性之前测试这些值。 就像在使用可选属性之前检查 undefined 一样,我们可以使用缩小来检查可能为 null 的值:
function doSomething(x: string | null) {
if (x === null) {
// 什么都不做
} else {
console.log("Hello, " + x.toUpperCase());
}
}Try非空断言运算符(后缀 !)
TypeScript 还有一种特殊语法,可以在不进行任何显式检查的情况下从类型中移除 null 和 undefined。 在任何表达式后写 ! 实际上是一种类型断言,表明该值不是 null 或 undefined:
就像其他类型断言一样,这不会改变代码的运行时行为,所以只有在您知道值不可能是 null 或 undefined 时才使用 !。
枚举
枚举是 TypeScript 添加到 JavaScript 的一个特性,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 特性不同,这不是对 JavaScript 的类型层面添加,而是添加到语言和运行时中的东西。因此,这是一个您应该知道它存在,但也许在确定需要之前暂缓使用的特性。您可以在枚举参考页面中阅读更多关于枚举的信息。
不太常见的原始类型
值得一提的是在类型系统中表示的 JavaScript 的其余原始类型。 虽然我们不会在此深入探讨。
bigint
从 ES2020 开始,JavaScript 中有一种用于非常大整数的原始类型 BigInt:
// 通过 BigInt 函数创建一个 bigint
const oneHundred: bigint = BigInt(100);
// 通过字面量语法创建一个 BigInt
const anotherHundred: bigint = 100n;Try您可以在 TypeScript 3.2 发布说明中了解更多关于 BigInt 的信息。
symbol
JavaScript 中有一种原始类型,通过函数 Symbol() 创建一个全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) { // 永远不会发生
}Try您可以在符号参考页面中了解更多关于它们的信息。