# 日常类型

在本章中,我们将介绍 JavaScript 代码中一些最常见的值类型,并解释在 TypeScript 中描述这些类型的相应方法。 这不是一个详尽的列表,未来的章节将描述更多命名和使用其他类型的方法。

类型也可以出现在更多的地方,而不仅仅是类型注释。 当我们了解类型本身时,我们还将了解可以引用这些类型以形成新结构的地方。

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

# 原语:string、number 和 boolean

JavaScript 有三个非常常用的 原语stringnumberboolean。 每个在 TypeScript 中都有对应的类型。 如您所料,如果您对这些类型的值使用 JavaScript typeof运算符,这些名称与您看到的名称相同:

  • string表示字符串值,如 "Hello, world"
  • number代表像 42这样的数字。JavaScript 对整数没有特殊的运行时值,因此没有等价于 intfloat- 一切都只是 number
  • boolean代表 truefalse这两个值

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

# 数组

要指定像 [1, 2, 3] 这样的数组的类型,可以使用语法 number[];此语法适用于任何类型(例如 string[] 是字符串数组,等等)。 你也可以看到这个写成 Array<number&gt;,意思是一样的。 当我们介绍泛型时,我们将了解更多关于语法 T&lt;U&gt; 的信息。

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

# any

TypeScript 也有一个特殊的类型,any,当你不希望某个特定的值导致类型检查错误时,你可以使用它。

当一个值的类型为 any时,您可以访问它的任何属性(这又将是 any类型),像函数一样调用它,将它分配给(或从)任何类型的值,或者几乎任何其他东西这在语法上是合法的:

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;

当你不想写出一个长类型来让 TypeScript 相信特定的代码行是可以的时,any类型很有用。

# noImplicitAny

当你没有指定类型,并且 TypeScript 不能从上下文推断它时,编译器通常会默认为 any

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

# 变量的类型注释

当您使用 constvarlet声明变量时,您可以选择添加类型注释以显式指定变量的类型:

let myName: string = "Alice";
//        ^^^^^^^^ Type annotation

TypeScript 不使用像 int x = 0;这样的 "types on the left" 风格的声明 类型注释将始终在输入的内容之后。

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

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

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

# 函数

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

# 参数类型注解

声明函数时,可以在每个参数后面加上类型注解,声明函数接受哪些类型的参数。 参数类型注释在参数名称之后:

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

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

declare function greet(name: string): void;

// Would be a runtime error if executed!
greet(42);

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

# 返回类型注解

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

function getFavoriteNumber(): number {
  //                        ^^^^^^^^
  return 26;
}

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

# 匿名函数

匿名函数与函数声明有点不同。 当一个函数出现在 TypeScript 可以确定如何调用它的地方时,该函数的参数会自动被赋予类型。

这是一个例子:

// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];

// Contextual typing for function
names.forEach(function (s) {
  console.log(s.toUppercase());
});

// Contextual typing also applies to arrow functions
names.forEach((s) => {
  console.log(s.toUppercase());
});

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

这个过程称为上下文类型,因为函数发生的上下文告知它应该具有什么类型。

与推理规则类似,您不需要明确了解这是如何发生的,但了解它确实发生可以帮助您注意到何时不需要类型注释。 稍后,我们将看到更多关于值出现的上下文如何影响其类型的示例。

# 对象类型

除了原语之外,您会遇到的最常见的类型是对象类型。 这指的是任何带有属性的 JavaScript 值,几乎是所有属性! 要定义对象类型,我们只需列出其属性及其类型。

例如,这是一个接受点状对象的函数:

// 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 });

在这里,我们使用具有两个属性的类型注释参数 - xy- 这两个属性都是 number类型。 您可以使用 ,;来分隔属性,最后一个分隔符是可选的。

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

# 可选属性

对象类型还可以指定它们的部分或全部属性是可选的。 为此,请在属性名称后添加 ?

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

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

function printName(obj: { first: string; last?: string }) {
  // Error - might crash if 'obj.last' wasn't provided!
  console.log(obj.last.toUpperCase());
  if (obj.last !== undefined) {
    // OK
    console.log(obj.last.toUpperCase());
  }

  // A safe alternative using modern JavaScript syntax:
  console.log(obj.last?.toUpperCase());
}

# 联合类型

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

# 定义联合类型

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

让我们编写一个可以对字符串或数字进行操作的函数:

function printId(id: number | string) {
  console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
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") {
    // In this branch, id is of type 'string'
    console.log(id.toUpperCase());
  } else {
    // Here, id is of type 'number'
    console.log(id);
  }
}

另一个例子是使用像 Array.isArray这样的函数:

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

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

有时你会有一个联合,所有成员都有共同点。 例如,数组和字符串都有一个 slice方法。 如果联合中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小作用域:

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

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

# 类型别名

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

类型别名就是这样 - 任何类型的名称。 类型别名的语法是:

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

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

type ID = number | string;

请注意,别名只是别名 - 您不能使用类型别名来创建相同类型的不同/不同 "versions"。 当您使用别名时,就好像您已经编写了别名类型。 换句话说,这段代码可能看起来非法,但根据 TypeScript 是可以的,因为这两种类型都是同一类型的别名:

declare function getInput(): string;
declare function sanitize(str: string): string;

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";

# 接口

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

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 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");

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

TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。 此规则可防止 "impossible" 强制,例如:

const x = "hello" as number;

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

declare const expr: any;
type T = { a: 1; b: 2; c: 3 };

const a = (expr as any) as T;

# 字面类型

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

考虑这一点的一种方法是考虑 JavaScript 如何使用不同的方法来声明变量。varlet都允许更改变量中保存的内容,而 const不允许。这反映在 TypeScript 如何为字面创建类型。

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;
// ^?

const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
// ^?

就其本身而言,字面类型并不是很有价值:

let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";

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

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

function printText(s: string, alignment: "left" | "right" | "center") {
  // ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");

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

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");

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

# 字面推理

当您使用对象初始化变量时,TypeScript 假定该对象的属性可能会在以后更改值。 例如,如果您编写如下代码:

declare const someCondition: boolean;

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);

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

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

  • 您可以通过在任一位置添加类型断言来更改推理:
declare function handleRequest(url: string, method: "GET" | "POST"): void;

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

更改 1 表示 "I intend for req.methodto always have the literal type"GET"",防止之后可能将 "GUESS"分配给该字段。 更改 2 表示 "I know for other reasons that req.methodhas the value "GET""。

  • 您可以使用 as const将整个对象转换为类型字面:
declare function handleRequest(url: string, method: "GET" | "POST"): void;

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

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

# null 和 undefined

JavaScript 有两个原始值用于表示不存在或未初始化的值:nullundefined

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

# strictNullChecks 关闭

关闭 strictNullChecks ,可能是 nullundefined的值仍然可以正常访问,并且值 nullundefined可以分配给任何类型的属性。 这类似于没有空检查的语言(例如 C#、Java)的行为方式。 缺乏检查这些值往往是错误的主要来源;如果在他们的代码库中这样做是可行的,我们总是建议人们打开 strictNullChecks

# strictNullChecks 开启

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

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

# 非空断言运算符(后缀 !)

TypeScript 还具有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 nullundefined。 在任何表达式之后写 !实际上是一个类型断言,该值不是 nullundefined

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

就像其他类型断言一样,这不会改变代码的运行时行为,所以当你知道值不能是 nullundefined时,只使用 !很重要。

# 枚举

枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 功能不同,这不是对 JavaScript 的类型级添加,而是添加到语言和运行时的东西。正因为如此,这是一个你应该知道存在的功能,但除非你确定,否则可能会推迟使用。您可以在 枚举参考页 中阅读有关枚举的更多信息。

# 不太常见的原语

值得一提的是类型系统中表示的 JavaScript 中的其他原语。 虽然我们不会在这里深入。

# bigint

从 ES2020 开始,JavaScript 中有一个原语用于非常大的整数,BigInt

// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);

// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;

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

# symbol

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

const firstName = Symbol("name");
const secondName = Symbol("name");

if (firstName === secondName) {
  // Can't ever happen
}

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

Last Updated: 5/25/2023, 2:35:11 PM