日常类型

在本章中,我们将介绍在 JavaScript 代码中会遇到的最常见的一些值类型,并说明在 TypeScript 中描述这些类型的方法。这不是一个详尽的列表,后续章节将介绍更多命名和使用其他类型的方法。

类型还可以出现在比类型注释更多的地方。当我们了解类型本身时,我们还将了解可以在其中引用这些类型以形成新构造的地方。

我们首先回顾一下编写 JavaScript 或 TypeScript 代码时可能会遇到的最基本且最常见的类型。这些类型稍后将构成更复杂类型的核心构建块。

基元:stringnumberboolean

JavaScript 有三个非常常用的 基元stringnumberboolean。每个在 TypeScript 中都有对应的类型。正如你所料,这些名称与使用 JavaScript typeof 运算符对这些类型的某个值时看到的名称相同

  • string 表示字符串值,例如 "Hello, world"
  • number 用于表示数字,例如 42。JavaScript 没有针对整数的特殊运行时值,因此没有等效于 intfloat 的类型——所有内容都只是 number
  • boolean 用于表示两个值 truefalse

类型名称 StringNumberBoolean(以大写字母开头)是合法的,但它们指的是一些特殊的内置类型,这些类型在你的代码中很少出现。始终对类型使用 stringnumberboolean

数组

要指定数组的类型,例如 [1, 2, 3],可以使用语法 number[];此语法适用于任何类型(例如 string[] 是字符串数组,依此类推)。你还可以看到它写成 Array<number>,其含义相同。我们将在介绍泛型时详细了解语法 T<U>

请注意,[number] 是另一回事;请参阅 元组部分。

any

TypeScript 还有一种特殊类型,any,在你不希望某个值导致类型检查错误时可以使用。

当一个值属于类型 any 时,你可以访问它的任何属性(这些属性反过来也属于类型 any),像调用函数一样调用它,将其赋值给(或从)任何类型的某个值,或者执行其他任何语法上合法的事情

ts
let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
Try

当你不希望写出较长的类型来让 TypeScript 相信某一行代码是正确的时,any 类型非常有用。

noImplicitAny

当你未指定类型,并且 TypeScript 无法从上下文中推断出类型时,编译器通常会默认为 any

不过,你通常希望避免这种情况,因为 any 不会进行类型检查。使用编译器标志 noImplicitAny 将任何隐式 any 标记为错误。

变量上的类型注释

当你使用 constvarlet 声明变量时,可以选择添加类型注释来明确指定变量的类型

ts
let myName: string = "Alice";
Try

TypeScript 不使用“左侧类型”风格的声明,如 int x = 0; 类型注释始终位于待类型化的内容之后

不过,在大多数情况下,这是不需要的。在可能的情况下,TypeScript 会尝试自动推断代码中的类型。例如,变量的类型是根据其初始化程序的类型推断出来的

ts
// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";
Try

在大多数情况下,你不需要明确学习推断规则。如果你刚开始,请尝试使用比你认为需要的更少的类型注释 - 你可能会惊讶于你只需要很少的类型注释就能让 TypeScript 完全理解正在发生的事情。

函数

函数是 JavaScript 中传递数据的主要方式。TypeScript 允许你指定函数的输入和输出值的类型。

参数类型注释

声明函数时,可以在每个参数后添加类型注释,以声明函数接受的参数类型。参数类型注释位于参数名称之后

ts
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
Try

当参数具有类型注释时,将检查该函数的参数

ts
// Would be a runtime error if executed!
greet(42);
Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.
Try

即使参数没有类型注释,TypeScript 仍会检查您是否传递了正确数量的参数。

返回类型注释

您还可以添加返回类型注释。返回类型注释出现在参数列表之后

ts
function getFavoriteNumber(): number {
return 26;
}
Try

与变量类型注释非常相似,您通常不需要返回类型注释,因为 TypeScript 会根据其 return 语句推断函数的返回类型。上述示例中的类型注释不会改变任何内容。一些代码库将明确指定返回类型以用于文档目的,防止意外更改,或仅仅出于个人偏好。

返回 Promise 的函数

如果您想注释返回 Promise 的函数的返回类型,则应使用 Promise 类型

ts
async function getFavoriteNumber(): Promise<number> {
return 26;
}
Try

匿名函数

匿名函数与函数声明有点不同。当函数出现在 TypeScript 可以确定其调用方式的位置时,该函数的参数将自动获得类型。

以下是一个示例

ts
const names = ["Alice", "Bob", "Eve"];
 
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
console.log(s.toUpperCase());
});
 
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUpperCase());
});
Try

即使参数 s 没有类型注释,TypeScript 也使用了 forEach 函数的类型以及数组的推断类型来确定 s 将具有的类型。

此过程称为上下文类型化,因为函数所在的上下文会告知其应具有的类型。

与推断规则类似,您无需明确了解此过程如何发生,但了解它确实发生可以帮助您注意到何时不需要类型注释。稍后,我们将看到更多有关值所在的上下文如何影响其类型的示例。

对象类型

除了基本类型之外,您将遇到的最常见的类型是对象类型。这指的是具有属性的任何 JavaScript 值,几乎所有值都是如此!要定义对象类型,我们只需列出其属性及其类型。

例如,这是一个采用类似于点的对象作为参数的函数

ts
// The parameter's type annotation is an object type
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 });
Try

在此,我们使用具有两个属性(xy)的类型对参数进行注释,这两个属性均为 number 类型。您可以使用 ,; 来分隔属性,并且最后一个分隔符是可选的。

每个属性的类型部分也是可选的。如果你不指定类型,则假定为any

可选属性

对象类型还可以指定其部分或全部属性为可选。为此,在属性名称后添加一个?

ts
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
Try

在 JavaScript 中,如果你访问一个不存在的属性,你将获得值undefined,而不是运行时错误。因此,当你从可选属性中读取时,你必须在使用之前检查undefined

ts
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
'obj.last' is possibly 'undefined'.18048'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
 
// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}
Try

联合类型

TypeScript 的类型系统允许你使用各种运算符从现有类型构建新类型。现在我们知道了如何编写一些类型,是时候开始以有趣的方式组合它们了。

定义联合类型

你可能看到的组合类型的第一个方法是联合类型。联合类型是由两个或更多其他类型形成的类型,表示可能为其中任何一个类型的值。我们将这些类型中的每一个称为联合的成员

让我们编写一个可以在字符串或数字上运行的函数

ts
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.2345Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
Try

使用联合类型

提供与联合类型匹配的值很容易 - 只需提供与联合的任何成员匹配的类型。如果您拥有联合类型的值,您将如何使用它?

TypeScript 仅允许在联合的每个成员有效的情况下执行操作。例如,如果您有联合 string | number,则不能使用仅在 string 上可用的方法

ts
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'.2339Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
}
Try

解决方案是用代码缩小联合,就像在没有类型注释的情况下在 JavaScript 中所做的那样。当 TypeScript 可以根据代码结构为值推断出更具体的类型时,就会发生缩小。

例如,TypeScript 知道只有 string 值才会有 typeof"string"

ts
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
Try

另一个示例是使用类似 Array.isArray 的函数

ts
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
Try

请注意,在 else 分支中,我们不需要做任何特殊的事情 - 如果 x 不是 string[],那么它一定是 string

有时您会遇到所有成员都有一些共同点的联合。例如,数组和字符串都具有 slice 方法。如果联合中的每个成员都有一个公共属性,则可以在不缩小的情况下使用该属性

ts
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
Try

令人困惑的是,类型的联合似乎具有这些类型的属性的交集。这不是偶然 - 名称联合来自类型理论。联合 number | string 是通过获取每个类型的值的联合来组成的。请注意,给定两个具有关于每个集合的相应事实的集合,只有这些事实的交集适用于集合本身的联合。例如,如果我们有一间戴着帽子的高个子房间,另一间戴着帽子的西班牙语房间,在合并这些房间后,我们唯一了解每个人的事情就是他们必须戴着帽子。

类型别名

我们一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但通常需要多次使用同一类型并通过单个名称来引用它。

类型别名正是如此 - 任何类型名称。类型别名的语法是

ts
type Point = {
x: number;
y: number;
};
 
// Exactly the same as the earlier example
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 });
Try

实际上,你可以使用类型别名来命名任何类型,而不仅仅是对象类型。例如,类型别名可以命名联合类型

ts
type ID = number | string;
Try

请注意,别名是别名 - 你不能使用类型别名来创建同一类型的不同/独立“版本”。当你使用别名时,它就像你写了别名类型一样。换句话说,这段代码可能看起来是非法的,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名

ts
type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
 
// Create a sanitized input
let userInput = sanitizeInput(getInput());
 
// Can still be re-assigned with a string though
userInput = "new input";
Try

接口

接口声明是命名对象类型的另一种方式

ts
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 });
Try

就像我们在上面使用类型别名一样,这个示例就像我们使用了匿名对象类型一样。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 会告诉你是否需要将其作为另一种声明。如果你想要一个启发式方法,请使用 interface,直到你需要使用 type 中的功能为止。

ts

与类型注释类似,类型断言会被编译器删除,并且不会影响代码的运行时行为。

您还可以使用尖括号语法(除非代码位于 .tsx 文件中),这与之等效

ts
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
Try

提醒:由于类型断言在编译时被删除,因此没有与类型断言关联的运行时检查。如果类型断言错误,则不会生成异常或 null

TypeScript 仅允许将类型转换为更具体或更不具体的类型的类型断言。此规则防止了“不可能”的强制转换,例如

ts
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.2352Conversion 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.
Try

有时此规则可能过于保守,并且将不允许可能有效的更复杂的强制转换。如果发生这种情况,您可以使用两个断言,首先转换为 any(或 unknown,我们将在后面介绍),然后转换为所需的类型

ts
const a = expr as any as T;
Try

文字类型

除了常规类型 stringnumber 外,我们还可以引用类型位置中的特定字符串和数字。

考虑这个问题的一种方法是考虑 JavaScript 如何提供声明变量的不同方法。varlet 都允许更改变量中保存的内容,而 const 则不允许。这反映在 TypeScript 如何为文字创建类型的方式中。

ts
let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
let changingString: string
 
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
const constantString: "Hello World"
Try

从它们自身来看,字面量类型并没有多大价值

ts
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.
Try

拥有一个只能有一个值的变量并没有多大用处!

但通过将字面量组合成联合,你可以表达一个更有用的概念 - 例如,仅接受一组已知值的函数

ts
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"'.2345Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
Try

数字字面量类型的工作方式相同

ts
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
Try

当然,你可以将它们与非字面量类型组合

ts
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"'.2345Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
Try

还有一种字面量类型:布尔字面量。只有两种布尔字面量类型,正如你可能猜到的,它们是类型truefalse。类型boolean本身实际上只是联合true | false的别名。

字面量推断

当你使用对象初始化变量时,TypeScript 假设该对象的属性值稍后可能会发生变化。例如,如果你编写如下代码

ts
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
Try

TypeScript 不会假设将1赋值给之前具有0的字段是一个错误。另一种说法是,obj.counter必须具有类型number,而不是0,因为类型用于确定读取写入行为。

字符串也适用

ts
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"'.2345Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
Try

在上面的示例中,req.method被推断为string,而不是"GET"。因为代码可以在创建req和调用handleRequest之间进行评估,这可能会将一个新字符串(如"GUESS")分配给req.method,所以 TypeScript 认为这段代码有错误。

有两种方法可以解决这个问题。

  1. 你可以通过在任一位置添加类型断言来更改推断

    ts
    // Change 1:
    const req = { url: "https://example.com", method: "GET" as "GET" };
    // Change 2
    handleRequest(req.url, req.method as "GET");
    Try

    更改 1 表示“我打算让req.method始终具有字面量类型"GET"”,防止之后将"GUESS"可能分配给该字段。更改 2 表示“我出于其他原因知道req.method的值为"GET"”。

  2. 你可以使用as const将整个对象转换为字面量类型

    ts
    const req = { url: "https://example.com", method: "GET" } as const;
    handleRequest(req.url, req.method);
    Try

as const后缀的作用类似于const,但适用于类型系统,确保所有属性都分配了字面量类型,而不是更通用的版本,如stringnumber

nullundefined

JavaScript 有两个原始值用于表示缺失或未初始化的值:nullundefined

TypeScript 有两个对应的同名类型。这些类型的行为取决于是否启用了 strictNullChecks 选项。

strictNullChecks 关闭

如果 strictNullChecks 关闭,则可能为 nullundefined 的值仍然可以正常访问,并且可以将值 nullundefined 赋值给任何类型的属性。这类似于没有空检查的语言(例如 C#、Java)的行为。缺少对这些值的检查往往是错误的主要来源;我们始终建议人们在代码库中切实可行的情况下启用 strictNullChecks

strictNullChecks 开启

strictNullChecks 开启 时,如果一个值是 nullundefined,在对该值使用方法或属性之前,需要测试这些值。就像在使用可选属性之前检查 undefined 一样,我们可以使用 缩小 来检查可能为 null 的值

ts
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Try

非空断言运算符(后缀 !

TypeScript 还具有一个特殊语法,用于从类型中删除 nullundefined,而无需进行任何显式检查。在任何表达式后编写 ! 实际上是对值不是 nullundefined 的类型断言

ts
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
Try

就像其他类型断言一样,这不会改变代码的运行时行为,因此只有在知道该值不能nullundefined 时才使用 ! 非常重要。

枚举

枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可以是可能命名的常量集合中的一个。与大多数 TypeScript 功能不同,这不是对 JavaScript 的类型级添加,而是添加到语言和运行时中的内容。因此,这是一项你应该知道存在的特性,但除非你确定,否则可能暂缓使用。你可以在 枚举参考页面 中阅读有关枚举的更多信息。

不常见的基元

值得一提的是 JavaScript 中其余的基元,它们在类型系统中表示。尽管我们不会在这里深入探讨。

bigint

从 ES2020 开始,JavaScript 中出现了一个用于处理超大整数的基元,BigInt

ts
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
 
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
Try

您可以在 TypeScript 3.2 发行说明 中了解有关 BigInt 的更多信息。

symbol

JavaScript 中有一个基元用于通过函数 Symbol() 创建全局唯一引用

ts
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.2367This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
Try

您可以在 Symbol 参考页 中了解有关它们的更多信息。

TypeScript 文档是一个开源项目。通过 发送拉取请求 帮助我们改进这些页面 ❤

此页面的贡献者
RCRyan Cavanaugh (56)
OTOrta Therox (22)
UGUtku Gultopu (3)
ABAndrew Branch (2)
DRDaniel Rosenwasser (2)
29+

上次更新时间:2024 年 3 月 21 日