TypeScript Office - 常用类型
# 常用类型
在本章中,我们将介绍一些在 JavaScript 代码中最常见的值类型,并解释在 TypeScript 中描述这些类型的相应方法。这不是一个详尽的列表,未来的章节将描述命名和使用其他类型的更多方法。
类型还可以出现在更多的地方,而不仅仅是类型注释。当我们了解类型本身时,我们还将了解可以引用这些类型以形成新结构的地方。
我们将首先回顾你在编写 JavaScript 或 TypeScript 代码时,可能遇到的最基本和最常见的类型,它是形成更复杂类型的核心构建块。
# TypeScript配置文件
为了方便学习,我们可以在一个单独的配置文件中保存命令行中的参数,应用 tsc 生成配置文件:
tsc --init
在项目根目录下生成了一个配置文件 tsconfig.json 。这里给出我们自己的配置:
{
"compilerOptions": {
/* Language and Environment */
"target": "es6",
/* Modules */
"rootDir": "./src",
/* Emit */
"outDir": "./dist",
/* Type Checking */
"strict": true,
}
}
2
3
4
5
6
7
8
9
10
11
12
# 基元类型
# 三个类型
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。
let str: string = 'hello typescript'
let num: number = 100
let bool: boolean = true
2
3
# 数组
数组是指定形如 [1, 2, 3]
数据,可以使用语法 number[] 来定义; 此语法适用于任何类型(例如 string[] ,字符串数组等)。
你也可以写成 Array。T 当介绍泛型时,我们将了解更多有关这个语法的更多信息。
let arr: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
2
# any
TypeScript 还有一个特殊类型 any ,当你不希望某个特定值导致类型检查错误时,可以使用它。
当一个值的类型是 any 时,可以访问它的任何属性,将它分配给任何类型的值,或者几乎任何其他语法上的东西都合法的:
let obj: any = { x: 0 };
// 以下代码行都不会抛出编译器错误。
// 使用'any'将禁用所有进一步的类型检查
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
2
3
4
5
6
7
8
当你不想写出很长的类型只是为了让 TypeScript 相信特定的代码行没问题时, any 类型很有用。
noImplicitAny
当不指定类型时,并且 TypeScript 无法从上下文推断它时,编译器通常会默认为 any。
但是,您通常希望避免这种情况,因为 any 没有进行类型检查。使用编译器标志 noImplicitAny 将任何隐式标记 any 为错误。这个配置我们在前面讲到过。
# 变量上的类型注释
当你使用 const,var 或声明变量时的 let,可以选择添加类型注释来显式指定变量的类型:
let myName: string = "Felixlu";
TypeScript 不使用「左边的类型」风格的声明,比如 int x = 0; 类型注解总是在被输入的东西之后。
但是,在大多数情况下,这不是必需的。只要有可能,TypeScript 就会尝试自动推断代码中的类型。例如,变量的类型是根据其初始化器的类型推断出来的:
// 不需要类型定义,myName 推断为类型 string
let myName = "Felixlu";
2
大多数情况下,不需要明确学习推理规则。如果你刚开始,请尝试使用比你想象的更少的类型注释,你可能会惊讶 TypeScript 完全了解正在发生的事情。
# 函数
函数是在 JavaScript 中传递数据的主要方式。TypeScript 允许您指定函数的输入和输出值的类型。
# 参数类型注释
声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注释位于参数名称之后:
// 参数类型定义
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
2
3
4
当参数具有类型注释时,将检查该函数的参数:
// 如果执行,将是一个运行时错误!
greet(42);
2
即使您的参数上没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。
# 返回类型注释
你还可以添加返回类型注释。返回类型注释出现在参数列表之后:
function getFavoriteNumber(): number {
return 26;
}
2
3
与变量类型注释非常相似,通常不需要返回类型注释,因为 TypeScript 会根据其 return 语句推断函数的返回类型。上面例子中的类型注释不会改变任何东西。某些代码库会出于文档目的明确指定返回类型,以防止意外更改或仅出于个人偏好。
# 匿名函数
匿名函数与函数声明有点不同。当一个函数出现在 TypeScript 可以确定它将如何被调用的地方时,该函数的参数会自动指定类型。
下面是一个例子:
// 这里没有类型注释,但是TypeScript可以发现错误
const names = ["Alice", "Bob", "Eve"];
// 函数上下文类型
names.forEach(function (s) {
console.log(s.toUppercase());
});
// 上下文类型也适用于箭头函数
names.forEach((s) => {
console.log(s.toUppercase());
})
2
3
4
5
6
7
8
9
10
即使参数 s 没有类型注释,TypeScript 也会使用 forEach 函数的类型,以及数组的推断类型来确定 s 的类型。
这个过程称为 上下文类型,因为函数发生在其中的上下文通知它应该具有什么类型。
与推理规则类似,你不需要明确了解这是如何发生的,但了解它的机制确实可以帮助你注意何时不需要类型注释。稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。
# 对象类型
除了 string,number,boolean 类型(又称基元类型)外,你将遇到的最常见的类型是对象类型。这指的是任何带有属性的 JavaScript 值,几乎是所有属性。要定义对象类型,我们只需列出其属性及其类型。
例如,这是一个接受点状对象的函数:
// 参数的类型注释是对象类型
function printCoord(pt: { x: number; y: number }) {
console.log("坐标的x值为: " + pt.x);
console.log("坐标的y值为: " + pt.y);
}
printCoord({ x: 3, y: 7 });
2
3
4
5
6
在这里,我们使用具有两个属性的类型注释参数 x 和 y,这两个属性都是 number 类型。你可以以使用 ,
或 ;
来分隔属性,最后一个分隔符是可选的。
每个属性的类型部分也是可选的。如果你不指定类型,则将假定为 any 。
# 可选属性
对象类型还可以指定其部分或全部属性是可选的。为此,请在属性名称后添加一个 ? :
function printName(obj: { first: string; last?: string }) {
// ...
}
// 两种传递参数都可以
printName({ first: "Felix" });
printName({ first: "Felix", last: "Lu" });
2
3
4
5
6
在 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());
}
2
3
4
5
6
7
8
9
10
# 联合类型
TypeScript 的类型系统允许你使用多种运算符,从现有类型中构建新类型。现在我们知道如何编写几种类型,是时候开始以有趣的方式组合它们了。
# 定义联合类型
第一种组合类型的方法是联合类型。联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中的任何一种的值。我们将这些类型中的每一种称为联合类型的成员。
让我们编写一个可以对字符串或数字进行操作的函数:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// 正确
printId(101);
// 正确
printId("202");
// 错误
printId({ myID: 22342 });
2
3
4
5
6
7
8
9
# 使用联合类型
提供匹配联合类型的值很容易,只需提供匹配任何联合成员的类型。如果你有一个联合类型的值,你如何使用它?
如果联合的每个成员都有效,TypeScript 将只允许你使用联合做一些事情。例如,如果你有联合类型 string | number,则不能只使用一种类型的操作,比如 string:
function printId(id: number | string) {
console.log(id.toUpperCase());
}
2
3
解决方案是用代码缩小联合,就像在没有类型注释的 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);
}
}
2
3
4
5
6
7
8
9
另一个例子是使用如下函数 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);
}
}
2
3
4
5
6
7
8
9
请注意,在 else 分支中,我们不需要做任何特别的事情,如果 x 不是 string[]
,那么它一定是 string。
有时你会有一个 union,所有成员都有一些共同点。例如,数组和字符串都有一个 slice 方法。如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小范围:
// 返回类型推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
2
3
4
# 类型别名
我们一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但是想要多次使用同一个类型,并用一个名称来引用它是很常见的。
一个类型别名正是一个名称为任何类型的定义。类型别名的语法是:
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 });
2
3
4
5
6
7
8
9
10
实际上,你可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型:
type ID = number | string;
请注意,别名只是别名,你不能使用类型别名来创建相同类型的不同〉版本」。当你使用别名时,就像你编写了别名类型一样。换句话说,这段代码可能看起来不合法,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名:
type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return str.slice(0, 2)
}
// 创建经过 sanitize 的输入
let userInput = sanitizeInput('hello');
// 但仍可以使用字符串重新分配值
userInput = "new input";
2
3
4
5
6
7
8
# 接口
一个接口声明是另一种方式来命名对象类型:
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 });
2
3
4
5
6
7
8
9
就像我们在上面使用类型别名时一样,该示例就像我们使用了匿名对象类型一样工作。TypeScript 只关心我们传递给的值的结构 printCoord,它只关心它是否具有预期的属性。只关心类型的结构和功能,是我们将 TypeScript 称为结构类型类型系统的原因。
# 类型别名和接口之间的差异
类型别名和接口非常相似,在很多情况下你可以自由选择它们。几乎所有的功能都在 interface 中可用 type,关键区别在于扩展新类型的方式不同:
// 扩展接口
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: 'winnie',
honey: true
}
bear.name
bear.honey
2
3
4
5
6
7
8
9
10
11
12
13
通过交叉点扩展类型
// 通过交叉点扩展类型
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear: Bear = {
name: 'winnie',
honey: true
}
bear.name;
bear.honey;
2
3
4
5
6
7
8
9
10
11
12
13
向现有接口添加新字段
// 向现有接口添加新字段
interface MyWindow {
title: string
}
interface MyWindow {
count: number
}
const w: MyWindow = {
title: 'hello ts',
count: 100
}
2
3
4
5
6
7
8
9
10
11
类型创建后不可更改
// 类型创建后不可更改
type MyWindow = {
title: string
}
type MyWindow = {
count: number
}
2
3
4
5
6
7
- 在 TypeScript 4.2 版之前,类型别名可能出现在错误消息中,有时会代替等效的匿名类型(这可能是可取的,也可能是不可取的)。接口将始终在错误消息中命名
- 类型别名可能不参与声明合并,但接口可以
- 接口只能用于声明对象的形状,不能重命名基元
- 接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时
大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你是否需要其他类型的声明。如果您想要启发式,请使用 interface,然后在需要时使用 type。
# 类型断言
有时,你会获得有关 TypeScript 不知道的值类型的信息。
例如,如果你正在使用 document.getElementById
,TypeScript 只知道这将返回某种类型的 HTMLElement,但你可能知道你的页面将始终具有 HTMLCanvasElement 给定 ID 的值。
在这种情况下,你可以使用类型断言来指定更具体的类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。
还可以使用尖括号语法(除非代码在 .tsx 文件中),它是等效的:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
提醒:因为类型断言在编译时被移除,所以没有与类型断言相关联的运行时检查。null 如果类型断言错误,则不会出现异常。
TypeScript 只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止「不可能」的强制,例如:
const x = "hello" as number;
将类型 string 转换为类型 number 可能是错误的,因为两种类型都没有充分重叠。如果这是有意的,请先将表达式转换为 any 或 unknown(unknown,我们将在后面介绍),然后是所需的类型:
const x = ("hello" as unknown) as number;
# 文字类型
除了一般类型 string 和 number,我们可以在类型位置引用特定的字符串和数字。
一种方法是考虑 JavaScript 如何以不同的方式声明变量。var 和 let 两者都允许更改变量中保存的内容,const 不允许,这反映在 TypeScript 如何为文字创建类型上。
let testString = "Hello World";
testString = "Olá Mundo";
// 'testString' 可以表示任何可能的字符串,那
// TypeScript 是如何在类型系统中描述它的
testString;
const constantString = "Hello World";
// 因为 'constantString' 只能表示 1 个可能的字符串,所以
// 具有文本类型表示
constantString;
2
3
4
5
6
7
8
9
就其本身而言,文字类型并不是很有价值:
let x: "hello" = "hello";
// 正确
x = "hello";
// 错误
x = "howdy";
2
3
4
5
拥有一个只能有一个值的变量并没有多大用处。
但是通过将文字组合成联合,你可以表达一个更有用的概念,例如,只接受一组特定已知值的函数:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
2
3
4
5
数字文字类型的工作方式相同:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
2
3
当然,你可以将这些与非文字类型结合使用:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
2
3
4
5
6
7
8
9
还有一种文字类型:布尔文字。只有两种布尔文字类型,它们是类型 true 和 false 。类型 boolean 本身实际上只是联合类型 union 的别名 true | false。
# 文字推理
当你使用对象初始化变量时,TypeScript 假定该对象的属性稍后可能会更改值。例如,如果你写了这样的代码:
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
2
3
4
TypeScript 不假定先前具有的字段值 0,后又分配 1 是错误的。另一种说法是 obj.counter
必须有 number 属性,而非是 0,因为类型用于确定读取和写入行为。
这同样适用于字符串:
function handleRequest(url: string, method: 'GET' | 'POST' | 'GUESS') {
// ...
}
const req = { url: 'https://example.com', method: 'GET' };
handleRequest(req.url, req.method);
2
3
4
5
在上面的例子 req.method
中推断是 string,不是 "GET"。因为代码可以在创建 req 和调用之间进行评估,TypeScript 认为这段代码有错误。
有两种方法可以解决这个问题。
可以通过在任一位置添加类型断言来更改推理
// 方案 1
const req = { url: "https://example.com", method: "GET" as "GET" };
// 方案 2
handleRequest(req.url, req.method as "GET");
2
3
4
方案 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);
2
该 as const
后缀就像 const 定义,确保所有属性分配的文本类型,而不是一个更一般的 string 或 number。
# null 和 undefined
JavaScript 有两个原始值用于表示不存在或未初始化的值:null 和 undefined。
TypeScript 有两个对应的同名类型。这些类型的行为取决于您是否设置 strictNullChecks
选择。
# strictNullChecks 关闭
使用 false,仍然可以正常访问的值,并且可以将值分配给任何类型的属性。这类似于没有空检查的语言(例如 C#、Java)的行为方式。缺乏对这些值的检查往往是错误的主要来源;如果在他们的代码库中这样做可行,我们总是建议大家打开。
# strictNullChecks 打开
使用 true,你需要在对该值使用方法或属性之前测试这些值。就像在使用可选属性之前检查一样,我们可以使用缩小来检查可能的值:
function doSomething(x: string | null) {
if (x === null) {
// 做一些事
} else {
console.log("Hello, " + x.toUpperCase());
}
}
2
3
4
5
6
7
非空断言运算符(
!
后缀)
TypeScript 也有一种特殊的语法 null,undefined,可以在不进行任何显式检查的情况下,从类型中移除和移除类型。!
在任何表达式之后写入实际上是一种类型断言,即该值不是 null or undefined
:
function liveDangerously(x?: number | null) {
// 正确
console.log(x!.toFixed());
}
2
3
4
就像其他类型断言一样,这不会更改代码的运行时行为,因此仅 !
当你知道该值不能是 null 或 undefined 时使用才是重要的。
# 枚举
枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 功能不同,这不是JavaScript 的类型级别的添加,而是添加到语言和运行时的内容。因此,你确定你确实需要枚举在做些事情,否则请不要使用。
// TS 代码
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 1
2
3
4
5
6
7
8
// 编译后的 JS 代码
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
2
3
4
5
6
7
8
9
10
# 不太常见的原语
值得一提的是 JavaScript 中一些较新的原语,它们在 TypeScript 类型系统中也实现了。我们先简单的看两个例子:
# bigint
从 ES2020 开始,JavaScript 中有一个用于非常大的整数的原语 BigInt:
// 通过 bigint 函数创建 bigint
const oneHundred: bigint = BigInt(100);
// 通过文本语法创建 bigInt
const anotherHundred: bigint = 100n;
2
3
4
你可以在 TypeScript 3.2 发行说明 (opens new window) 中了解有关 BigInt 的更多信息。
# symbol
JavaScript 中有一个原语 Symbol()
,用于通过函数创建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这里的代码不可能执行
}
2
3
4
5
此条件将始终返回 false,因为类型 typeof firstName
和 typeof secondName
没有重叠。