免责声明: 本文为翻译自 Typescript 官方手册内容,非原创。版权归原作者所有。
原文来源: Everyday Types
翻译者注: 本人在翻译过程中已尽最大努力确保内容的准确性和完整性,但翻译工作难免有疏漏。如读者在阅读中发现任何问题,欢迎提出建议或直接查阅原文以获取最准确的信息。
在本章中,我们将介绍一些在 JavaScript 代码中最常见的值类型,并解释如何在 TypeScript 中描述这些类型。这不是一个详尽的列表,以后的章节将描述更多命名和使用其他类型的方法。
类型不仅可以出现在类型注释中,还可以出现在许多其他地方。在我们了解类型本身的同时,我们还将了解在哪些地方可以引用这些类型来形成新的结构。
我们将首先回顾编写 JavaScript 或 TypeScript 代码时可能遇到的最基本和常见的类型。这些稍后将成为更复杂类型的核心构建模块。
基本类型:string
,number
,和 boolean
JavaScript 有三种非常常用的原始类型:字符串(string
)、数字(number
)和布尔值(boolean
)。在 TypeScript 中,每种类型都有对应的类型。正如你所预期的那样,如果你在这些类型的值上使用 JavaScript typeof
运算符,你会看到相同的名称。
string
: 表示像"Hello, world"
这样的字符串值number
: 表示像 42 这样的数值。JavaScript 没有针对整数的特殊运行时值,因此没有等同于int
或float
的概念 - 一切都是简单地number
类型boolean
: 表示true
和false
两个值
类型名称 String、Number 和 Boolean(以大写字母开头)是合法的,但它们指的是一些特殊的内置类型,在你的代码中很少出现。始终使用 string、number 或 boolean 来表示类型。
数组
要指定一个类似 [1, 2, 3]
的数组的类型,可以使用语法 number[]
;这个语法适用于任何类型(例如 string[]
是字符串数组等)。你也可能会看到它写作 Array<number>
,意思是一样的。当我们学习泛型时,将更多地了解 T<U>
这种语法。
any
TypeScript 还有一个特殊类型,即 any
,如果你不希望特定值导致类型检查错误,就可以使用它。
当一个值的类型是 any
时,你可以访问它的任何属性(这些属性本身也会是 any
类型),像调用函数一样调用它,将其赋值给(或从中赋值)任意类型的值,或者几乎做任何其他在语法上合法的操作。
let obj: any = { x: 0 };
// 以下代码行不会引发编译器错误。
// 使用 `any` 关键字将禁用所有进一步的类型检查,假定你比 TypeScript 更了解环境。
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
当你不想只是为了让 TypeScript 相信某行特定代码没问题而编写长类型时,any
类型非常有用。
noImplicitAny
如果没有指定类型,并且 TypeScript 无法从上下文中推断出类型时,编译器通常会默认为 any
。
但是,通常应该避免使用 any
,因为它不进行类型检查。使用编译器标志 noImplicitAny 将隐式的 any
标记为错误。
变量的类型注释
当使用 const
、var
或 let
声明变量时,可以选择性地添加类型注释来明确指定变量的类型。
let myName: string = "Alice";
TypeScript 不使用“左侧类型”声明,例如 int x = 0; 类型注释总是在被注释的变量之后。
在大多数情况下,这是不必要的。TypeScript 会尽可能地自动推断代码中的类型。例如,变量的类型会根据其初始化值的类型进行推断:
// 不需要类型注释 -- 'myName' 推断为字符串类型
let myName = "Alice";
大部分情况下,你不需要刻意的去学习推断规则。如果你刚开始使用,请尝试使用比你想象的更少的类型注释 - 你可能会惊讶地发现,对于 TypeScript 来说,你所需的很少。
函数
函数是在 JavaScript 中传递数据的主要方式。TypeScript 允许你指定函数的输入和输出值的类型。
参数类型注释
当你声明一个函数时,可以在每个参数后面添加类型注释来声明函数接受的参数类型。参数类型注释放在参数名之后。
// 参数类型注释
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
当参数具有类型注释时,将对该函数的参数进行检查。
// 如果执行,将会出现运行时错误!
greet(42);
// Argument of type 'number' is not assignable to parameter of type 'string'.
即使参数没有类型注释,TypeScript 仍会检查传递的参数数量是否正确。
返回类型注释
你还可以添加返回类型注释。返回类型注释添加在参数列表之后:
function getFavoriteNumber(): number {
return 26;
}
和变量类型注释相同,通常不需要特意添加返回类型注释,因为 TypeScript 会根据函数的返回语句推断出其返回类型。上面示例中的类型注释并没有改变任何内容。某些代码库可能会明确指定返回类型以供方便阅读文档、防止意外更改或个人偏好。
返回 Promise 的函数
如果你想注释一个返回 Promise 的函数的返回类型,你应该使用 Promise 类型:
async function getFavoriteNumber(): Promise<number> {
return 26;
}
匿名函数
匿名函数与函数声明有些不同。当一个函数出现在 TypeScript 可以确定它将如何被调用的位置时,该函数的参数会自动获得类型。
这是一个例子:
const names = ["Alice", "Bob", "Eve"];
// 函数的上下文类型推断 - 推断参数 s 的类型为字符串
names.forEach(function (s) {
console.log(s.toUpperCase());
});
// 上下文类型推断也适用于箭头函数
names.forEach((s) => {
console.log(s.toUpperCase());
});
尽管参数 s
没有类型注释,但 TypeScript 使用 forEach
函数的类型以及数组的推断类型来确定 s
将具有的类型。
这个过程被称为上下文类型化,因为函数发生在其中的上下文会告诉它应该具有什么样的类型。
类似于推断规则,你不需要明确学习这是如何发生的,但了解它确实发生可以帮助你区分什么时候可以不需要类型注释。稍后,我们将看到更多关于值所处上下文如何影响其类型的示例。
对象类型
除了原始类型外,你会遇到的最常见的类型是对象。这指的是具有属性的任何 JavaScript 值,几乎都是如此!要定义一个对象类型,我们只需列出其属性及其类型。
例如,下面是一个接受坐标对象的函数:
// 参数的类型注释是一个对象类型
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
在这里,我们用两个属性 x
和 y
注释了带有类型的参数,它们都是 number
类型。你可以使用逗号或分号来分隔属性,最后一个参数的分隔符是可选的。
每个属性的类型部分也是可选的。如果不指定类型,则会假设为 any
。
可选的属性
对象类型还可以指定其某些或全部属性是可选的。要实现这一点,只需在属性名称后面添加一个 ?
:
function printName(obj: { first: string; last?: string }) {
// ...
}
// 都可以
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
在 JavaScript 中,如果访问一个不存在的属性,将得到值 undefined
而不是运行时错误。因此,在读取可选属性之前,你必须先检查是否为 undefined
再使用它。
function printName(obj: { first: string; last?: string }) {
// 错误 - 如果未提供 'obj.last',可能会崩溃!
console.log(obj.last.toUpperCase());
// 'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
// 使用现代JavaScript语法的安全替代方案:
console.log(obj.last?.toUpperCase());
}
联合类型
TypeScript 类型系统允许你使用各种运算符从现有类型去构建一个新类型。现在我们知道如何编写一些类型,是时候开始以有趣的方式组合它们了。
定义一个联合类型
第一种类型组合的方法是联合类型。联合类型是由两个或多个其他类型形成的一种类型,表示可能是其中任何一个类型的值。我们将这些类型称为联合的成员。
我们来编写一个可以操作字符串或数字的函数:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// 没问题
printId(101);
// 没问题
printId("202");
// 报错
printId({ myID: 22342 });
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
使用联合类型
提供与联合类型匹配的值很容易 - 只需提供与联合成员中任何一个匹配的类型即可。如果你有一个联合类型的值,如何处理它?
仅当操作对联合类型的每个成员都有效时,TypeScript 才会允许该操作。例如,如果有 string
| number
的联合,则无法使用仅在字符串上可用的方法:
function printId(id: number | string) {
console.log(id.toUpperCase());
// Property 'toUpperCase' does not exist on type 'string | number'.
// Property 'toUpperCase' does not exist on type 'number'.
}
解决方案是缩小代码的联合范围,就像在没有类型注释的 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);
}
}
另一个例子是使用类似于 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);
}
}
请注意,在 else
分支中,我们无需做任何特殊处理 - 如果 x
不是 string[]
,则一定是 string
。
有时候会遇到一种所有成员都具有共同特点的联合。例如,数组和字符串都有一个 slice
方法。如果联合中的每个成员都具有共同属性,则可以在不缩小范围的情况下使用该属性:
// 返回类型被推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
可能会让人感到困惑的是,类型的联合似乎具有这些类型属性的交集。这不是偶然 - union 一词来自于类型理论。联合 number | string 由每个类型中值的并集组成。请注意,给定两个集合以及每个集合的相应事实,只有这些事实的交集适用于集合本身的并集。例如,如果我们有一个房间,里面都是戴着帽子的高个子,另一个房间里讲西班牙语的人都戴着帽子,那么将这些房间组合起来后,我们对每个人唯一了解的就是他们一定戴着帽子。
类型别名
我们一直在使用对象类型和联合类型,通过直接在类型注释中编写它们。这很方便,但通常希望多次使用相同的类型,并通过一个名称引用它。
类型别名就是为任何类型取一个名称。类型别名的语法是:
type Point = {
x: number;
y: number;
};
// 与之前的示例完全相同
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
实际上可以使用类型别名来为任何类型命名,不仅限于对象类型。例如,类型别名可以给一个联合类型命名:
type ID = number | string;
请注意,别名只是别名 - 你不能使用类型别名来创建同一类型的不同“版本”。当你使用别名时,它与你编写的别名类型完全一样。换句话说,这段代码可能看起来非法,但在 TypeScript 中却是可以接受的,因为两个类型都是相同类型的别名:
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
// 创建一个经过清理的输入框
let userInput = sanitizeInput(getInput());
// 仍然可以使用字符串重新赋值
userInput = "new input";
接口
接口声明是另一种命名对象类型的方式:
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
就像我们在上面使用类型别名时一样,该示例的工作方式与我们使用匿名对象类型完全相同。TypeScript 只关心我们传递给 printCoord 的值的结构 - 它只关心它是否具有预期属性。只关注类型的结构和功能,这就是为什么我们称 TypeScript 为结构化类型系统的原因。
类型别名和接口之间的区别
类型别名和接口非常相似,在许多情况下,你可以可以在它们之间自由选择。几乎所有 interface
的特性在 type
中都是可用的,关键区别在于类型不能重新打开以添加新属性,而接口始终是可扩展的。
扩展接口
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
通过"&"扩展类型
type Animal = {
name: string;
}
type Bear = Animal & {
honey: boolean;
}
const bear = getBear();
bear.name;
bear.honey;
向现有接口添加新字段
interface Window {
title: string;
}
interface Window {
ts: TypeScriptAPI;
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
类型创建后不可再更改
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
在后面的章节中你会学到更多关于这些概念的知识,所以如果你当前没有理解这些知识,请不要担心。
- 在 TypeScript 4.2 之前,类型别名命名可能会出现在错误消息中,有时代替等效的匿名类型(可能需要也可能不需要)。接口在错误消息中将始终被命名。
- 类型别名不能参与声明合并,但接口可以。
- 接口只能用于 声明对象的形状,不能重命名基本类型.
- 接口名称将始终以其原始形式出现在错误消息中,但 只有 在按名称使用时才会出现。
在大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你它是否需要其他类型的声明。如果你想要启发式方法,可以使用 interface
直到你需要使用 type
中的功能。
类型断言
有时候,你会拥有 TypeScript 无法知晓的关于值类型的信息。
例如,如果你正在使用 document.getElementById
,TypeScript 只知道它将返回某种 HTMLElement
类型,但是你可能知道你的页面上总是会有一个具有特定 ID 的 HTMLCanvasElement。
在这种情况下,你可以使用类型断言来指定更具体的类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
与类型注释类似,类型断言会被编译器移除,并不会影响代码的运行时行为。
你还可以使用尖括号语法(代码必须在 .tsx 文件中),这是等效的:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
提醒:由于类型断言在编译时被移除,因此与类型断言相关的运行时检查是不存在的。如果类型断言错误,将不会生成异常或空值。
TypeScript 只允许类型断言将类型转换为更具体或不太具体的版本。这个规则防止了“不可能”的强制转换,例如:
const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently
// overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
有时候这个规则可能过于保守,会禁止一些可能是有效的更复杂的强制转换。如果发生这种情况,你可以使用两个断言,首先是到 any
(或 unknown
,在后面我们将介绍),然后再到所需的类型:
const a = expr as any as T;
字面量类型
除了一般的 string
和 number
类型,我们可以在类型位置引用特定的字符串和数字。
思考这个问题的一种方式是考虑 JavaScript 如何使用不同的方式来声明变量。var
和 let
都允许改变变量内部存储的内容,而 const
则不允许。TypeScript 对字面值创建类型时也反映了这一点。
let changingString = "Hello World";
changingString = "Olá Mundo";
// 因为`changingString`可以表示任何可能的字符串,这就是TypeScript在类型系统中描述它的方式
const constantString = "Hello World";
// 因为`constantString`只能表示一个可能的字符串,所以它具有字面类型表示。
字面量类型本身并不是很有价值:
let x: "hello" = "hello";
// 没问题
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.
一个只能有一个值的变量并没有多大用处!
但是通过将字面值组合成联合,可以表达一个更加有用的概念 - 例如,仅接受一组已知值集合的函数:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
// Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
数字字面量类型的工作方式相同:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
当然,你可以将这些与非字面类型结合使用:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
// Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
还有一种字面类型:布尔字面量。只有两种布尔字面量类型,你可能猜到了,它们就是 true
和 false
这两个类型。实际上,boolean
类型本身只是 true
| false
的联合别名。
字面量推断
当你使用一个对象初始化变量时,TypeScript 假设该对象的属性可能会在以后改变值。例如,如果你编写了以下代码:
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
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);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
在上面的示例中,req.method
被推断为 string
,而不是 "GET"
。因为在创建 req
和调用 handleRequest
之间可以评估代码,这段代码可能会将一个新的字符串(如"GUESS"
)赋给req.method
,TypeScript 认为这段代码存在错误。
有两种方法可以解决这个问题:
- 可以通过在任一位置添加类型断言来更改推断:
// 更改1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// 更改2
handleRequest(req.url, req.method as "GET");
更改1表示“我打算让 req.method
始终具有字面类型 "GET"
”,以防止在此字段之后可能将 "GUESS"
赋值给它。更改2表示“出于其他原因,我知道 req.method
的值为 "GET"
”。
- 你可以使用
as const
将整个对象转换为类型字面量:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
as const
后缀的作用类似于 const
,但是针对类型系统来说,确保所有属性都被赋予字面量类型,而不是更通用的版本,如 string
或 number
。
null
和 undefined
JavaScript 有两个原始值用于表示缺失或未初始化的值:null
和 undefined
。
TypeScript 有两个相应的同名类型。这些类型的行为取决于是否打开了 strictNullChecks 选项。
strictNullChecks
关闭
在关闭 strictNullChecks 的情况下,仍然可以正常访问可能为 null
或 undefined
的值,并且可以将 null
和 undefined
赋给任何类型的属性。这类似于没有 null 检查(例如C#,Java)的语言行为。不对这些值进行检查往往是 bug 的主要来源;我们始终建议人们在他们的代码库中实际可行时打开 strictNullChecks。
关闭 strictNullChecks 后,仍可以正常访问可能为 null 或 undefined 的值,并且可以将值 null 和 undefined 分配给任何类型的属性。 这类似于没有 null 检查的语言(例如 C#、Java)的行为方式。 缺乏对这些值的检查往往是错误的主要来源; 如果在代码库中可行的话,我们总是建议人们打开 strictNullChecks。
strictNullChecks
打开
启用 strictNullChecks 后,当一个值为 null
或 undefined
时,在使用该值的方法或属性之前,你需要先测试这些值。就像在使用可选属性之前检查是否为 undefined
一样,我们可以使用缩小范围来检查可能为 null
的值:
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
非空断言运算符(后缀 !
)
TypeScript 还有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中移除 null
和 undefined
。在表达式后面加上 !
实际上是一种类型断言,表示该值不为 null
或 undefined
:
function liveDangerously(x?: number | null) {
// 不报错
console.log(x!.toFixed());
}
就像其他类型断言一样,这不会改变代码的运行时行为,因此只有在你知道值不能为 null
或 undefined
时才使用 !
枚举
枚举是由 TypeScript 添加到 JavaScript 中的一个特性,它允许描述一个值,该值可以是一组可能的命名常量之一。与大多数 TypeScript 特性不同,这不是对 JavaScript 的类型级别进行的补充,而是添加到语言和运行时环境中的内容。因此,这是一个你应该知道的特性,但除非你确定需要使用它,否则尽可能的不要用。你可以在“枚举参考页面”上阅读更多关于枚举的信息。
不太常见的原始类型
值得一提的是类型系统中还有 JavaScript 其他原始数据类型,我们在这里不会进行深入讨论。
bigint
从 ES2020 开始,JavaScript 中有一个用于处理非常大的整数的原始类型,BigInt
:
// 通过 BigInt 函数创建一个大整数
const oneHundred: bigint = BigInt(100);
// 通过字面量语法创建一个 BigInt
const anotherHundred: bigint = 100n;
你可以在 TypeScript 3.2版本的发布说明 中了解有关 BigInt 的更多信息。
Symbol
在 JavaScript 中有一个原始类型,通过函数 Symbol()
来创建全局唯一的引用。
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// 不可能发生
}
你可以在 Symbols 参考页面 上了解更多相关信息。