本文中,小编将介绍一些在 JavaScript 代码中最常见的值类型,并解释在 TypeScript 中描述这些类型的相应方法。
1.基元类型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
。
let str: string = 'hello typescript'
let num: number = 100
let bool: boolean = true
2.数组
数组是指定形如[1, 2, 3]
数据,可以使用语法number[]
来定义; 此语法适用于任何类型(例如string[]
,字符串数组等)。
let arr: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
3.any
TypeScript 还有一个特殊类型 any
,当你不希望某个特定值导致类型检查错误时,可以使用它。
当一个值的类型是any
时,可以访问它的任何属性,将它分配给任何类型的值,或者几乎任何其他语法上的东西都合法的
let obj: any = { x: 0 };
// 以下代码行都不会抛出编译器错误。
// 使用'any'将禁用所有进一步的类型检查
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
4.变量上的类型注释
当你使用const
, var
, 或声明变量时let
,可以选择添加类型注释来显式指定变量的类型:
let myName: string = "Felixlu";
TypeScript 不使用“左边的类型”风格的声明,比如
int x = 0;
类型注解总是在被输入的东西之后。
但是,在大多数情况下,这不是必需的。只要有可能,TypeScript 就会尝试自动推断代码中的类型。例如,变量的类型是根据其初始化器的类型推断出来的:
// 不需要类型定义--“myName”推断为类型“string”
let myName = "Felixlu";
大多数情况下,不需要明确学习推理规则。如果你刚开始,请尝试使用比你想象的更少的类型注释 - 你可能会惊讶——TypeScript 完全了解正在发生的事情。
5.函数
函数是在 JavaScript 中传递数据的主要方式。TypeScript 允许您指定函数的输入和输出值的类型。
-
参数类型注释
声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注释位于参数名称之后:
// 参数类型定义
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
当参数具有类型注释时,将检查该函数的参数:
// 如果执行,将是一个运行时错误!
greet(42);
即使您的参数上没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。
-
返回类型注释
你还可以添加返回类型注释。返回类型注释出现在参数列表之后:
function getFavoriteNumber(): number {
return 26;
}
与变量类型注释非常相似,通常不需要返回类型注释,因为 TypeScript 会根据其return
语句推断函数的返回类型。上面例子中的类型注释不会改变任何东西。某些代码库会出于文档目的明确指定返回类型,以防止意外更改或仅出于个人偏好。
-
匿名函数
匿名函数与函数声明有点不同。当一个函数出现在 TypeScript 可以确定它将如何被调用的地方时,该函数的参数会自动指定类型。
下面是一个例子:
// 这里没有类型注释,但是TypeScript可以发现错误
const names = ["Alice", "Bob", "Eve"];
// 函数上下文类型
names.forEach(function (s) {
console.log(s.toUppercase());
});
// 上下文类型也适用于箭头函数
names.forEach((s) => {
console.log(s.toUppercase());
})
即使参数s
没有类型注释,TypeScript 也会使用forEach
函数的类型,以及数组的推断类型来确定s
的类型。
这个过程称为上下文类型,因为函数发生在其中的上下文通知它应该具有什么类型。
与推理规则类似,你不需要明确了解这是如何发生的,但了解它的机制确实可以帮助你注意何时不需要类型注释。稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。
6.对象类型
除了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 });
在这里,我们使用具有两个属性的类型注释参数 -x
和y
- 这两个属性都是 number
类型。你可以以使用,
或;
来分隔属性,最后一个分隔符是可选的。
每个属性的类型部分也是可选的。如果你不指定类型,则将假定为any
。
-
可选属性
对象类型还可以指定其部分或全部属性是可选的。为此,请在属性名称后添加一个?
:
function printName(obj: { first: string; last?: string }) {
// ...
}
// 两种传递参数都可以
printName({ first: "Felix" });
printName({ first: "Felix", last: "Lu" });
在 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());
}
7.联合类型
TypeScript 的类型系统允许你使用多种运算符,从现有类型中构建新类型。现在我们知道如何编写几种类型,是时候开始以有趣的方式组合它们了。
-
定义联合类型
第一种组合类型的方法是联合类型。联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中的任何一种的值。我们将这些类型中的每一种称为联合类型的成员。
让我们编写一个可以对字符串或数字进行操作的函数:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// 正确
printId(101);
// 正确
printId("202");
// 错误
printId({ myID: 22342 });
-
使用联合类型
提供匹配联合类型的值很容易- 只需提供匹配任何联合成员的类型。如果你有一个联合类型的值,你如何使用它?
如果联合的每个成员都有效,TypeScript 将只允许你使用联合做一些事情。例如,如果你有联合类型 string | number
,则不能只使用一种类型的操作,比如string
:
function printId(id: number | string) {
console.log(id.toUpperCase());
}
解决方案是用代码缩小联合,就像在没有类型注释的 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
。
有时你会有一个 union
,所有成员都有一些共同点。例如,数组和字符串都有一个slice
方法。如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小范围:
// 返回类型推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
8.类型别名
我们一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但是想要多次使用同一个类型,并用一个名称来引用它是很常见的。
一个类型别名正是一个名称为任何类型的定义。类型别名的语法是:
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 });
实际上,你可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类型:
type ID = number | string;
请注意,别名只是别名 - 你不能使用类型别名来创建相同类型的不同“版本”。当你使用别名时,就像你编写了别名类型一样。换句话说,这段代码可能看起来不合法,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名。
9.接口
一个接口声明是另一种方式来命名对象类型:
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 });
就像我们在上面使用类型别名时一样,该示例就像我们使用了匿名对象类型一样工作。TypeScript 只关心我们传递给的值的结构printCoord
——它只关心它是否具有预期的属性。只关心类型的结构和功能,是我们将 TypeScript 称为结构类型类型系统的原因。
-
在 TypeScript 4.2 版之前,类型别名可能出现在错误消息中,有时会代替等效的匿名类型(这可能是可取的,也可能是不可取的)。接口将始终在错误消息中命名。
-
类型别名可能不参与声明合并,但接口可以。
-
接口只能用于声明对象的形状,不能重命名基元。
-
接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时。
大多数情况下,你可以根据个人喜好进行选择,TypeScript 会告诉你是否需要其他类型的声明。如果您想要启发式,请使用interface
,然后在需要时使用type
。
10.类型断言
有时,你会获得有关 TypeScript 不知道的值类型的信息。
例如,如果你正在使用document.getElementById
,TypeScript 只知道这将返回某种类型的HTMLElement
,但你可能知道你的页面将始终具有HTMLCanvasElement
给定 ID 的值 。
在这种情况下,你可以使用类型断言来指定更具体的类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。
还可以使用尖括号语法(除非代码在.tsx
文件中),它是等效的:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
TypeScript 只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止“不可能”的强制,例如:
const x = "hello" as number;
将类型string
转换为类型number
可能是错误的,因为两种类型都没有充分重叠。如果这是有意的,请先将表达式转换为 any
或 unknown
(unknown
,我们将在后面介绍),然后是所需的类型
const x = ("hello" as unknown) as number;
11.文字类型
除了一般类型string
和number
,我们可以在类型位置引用特定的字符串和数字。
一种方法是考虑 JavaScript 如何以不同的方式声明变量。var
而let
两者都允许更改变量中保存的内容,const
不允许,这反映在 TypeScript 如何为文字创建类型上。
let testString = "Hello World";
testString = "Olá Mundo";
//'testString'可以表示任何可能的字符串,那
//TypeScript是如何在类型系统中描述它的
testString;
const constantString = "Hello World";
//因为'constantString'只能表示1个可能的字符串,所以
//具有文本类型表示
constantString;
就其本身而言,文字类型并不是很有价值:
let x: "hello" = "hello";
// 正确
x = "hello";
// 错误
x = "howdy";
12 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());
}
}
-
非空断言运算符(
!
后缀)
TypeScript 也有一种特殊的语法null
,undefined
,可以在不进行任何显式检查的情况下,从类型中移除和移除类型。!
在任何表达式之后写入实际上是一种类型断言,即该值不是null
or undefined
:
function liveDangerously(x?: number | null) {
// 正确
console.log(x!.toFixed());
}
就像其他类型断言一样,这不会更改代码的运行时行为,因此仅!
当你知道该值不能是null
或 undefined
时使用才是重要的。
13 枚举
枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 功能不同,这不是JavaScript 的类型级别的添加,而是添加到语言和运行时的内容。因此,你确定你确实需要枚举在做些事情,否则请不要使用。可以在Enum 参考页 中阅读有关枚举的更多信息。
// ts源码
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 1
// 编译后的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);
14 不太常见的原语
值得一提的是 JavaScript 中一些较新的原语,它们在 TypeScript 类型系统中也实现了。我们先简单的看两个例子:
-
bigint
从 ES2020 开始,JavaScript 中有一个用于非常大的整数的原语BigInt
:
// 通过bigint函数创建bigint
const oneHundred: bigint = BigInt(100);
// 通过文本语法创建BigInt
const anotherHundred: bigint = 100n;
-
symbol
JavaScript 中有一个原语Symbol()
,用于通过函数创建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这里的代码不可能执行
}
此条件将始终返回 false
,因为类型 typeof firstName
和typeof secondName
没有重叠。