本文是文档 Everyday Types 的翻译,对于了解 TypeScript 类型很有帮助,敬请阅读。

本文将介绍一些在 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>  的知识。

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

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 就会尝试自动推断代码中的类型。例如,变量的类型是根据其初始值设定项的类型推断的:

// 不需要显示指定类型 string ,TypeScript 会推断出 myName 为 string 类型
let myName = "Alice";

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

函数

函数是在 JavaScript 中传递数据的主要方式,TypeScript 可以指定函数的输入和输出的类型。

输入参数类型注释

当你声明一个函数时,你可以在每个参数后面加上类型注解来声明函数接受什么类型的参数。参数类型注释位于参数名称之后:

// 参数类型注释
function greet(name: string) {
  //                 ^^^^^^^^
  console.log("Hello, " + name.toUpperCase() + "!!");
}

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

// 类型报错
greet(42);

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

返回值类型注释

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

function getFavoriteNumber(): number {
  return 26;
}

与变量类型注释类似,通常不需要显示填写返回值的类型注释,因为 TypeScript 将根据 return 语句推断函数的返回值类型,上面示例中的类型注释写或不写是一样的。一些三方库会出于文档目的明确指定返回类型,防止意外变更。还有的可能出于个人喜好。

匿名函数

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

image.png

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

此过程称为上下文类型推断,因为函数发生的上下文告知它应该具有的类型。

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

对象类型

除了原始类型外,您会遇到的最常见的类型是对象类型。这是指任何具有属性的值,也就是几乎所有的值!要定义对象类型,我们只需列出其属性和其类型即可。

例如,有一个接收类似坐标点对象参数的函数:

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 }) {
  // 报错:如果没有 last 属性,那么这行将会报错
  console.log(obj.last.toUpperCase());
  if (obj.last !== undefined) {
    // 现在正确
    console.log(obj.last.toUpperCase());
  }
  // 或者可以使用现代 JavaScript 语法
  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  上可用的方法:

image.png

解决方案是通过代码缩小联合类型范围,就像在没有类型注释的 JavaScript 中一样。当 TypeScript 可以根据代码结构推断出更具体的类型时,就会发生类型范围缩小。

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

function printId(id: number | string) {
  if (typeof id === "string") {
    // 在这个 if 分支,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);
}

类型别名

上面一直在通过直接在类型注释中编写对象类型和联合类型来使用它们。通常这没问题,但想要多次使用同一类型并通过一个名称引用显然是更便捷的。

类型别名就可以做到:

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 规则它是可以的,因为这两种类型都是同一类型的别名:

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

type UserInputSanitizedString = string;

function sanitizeInput(str: string): UserInputSanitizedString {
  return sanitize(str);
}

// 创建一个清理后的输入
let userInput = sanitizeInput(getInput());

// 它仍然可以被赋值为 sting 类型
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

向现有接口添加新字段

interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});

通过交叉扩展类型

type Animal = {
  name: string
}

type Bear = Animal & {
  honey: boolean
}

const bear = getBear();
bear.name;
bear.honey;

类型创建后不能更改

type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

 // Error: Duplicate identifier 'Window'.

如果您当前不能理解如上代码,请不要担心。在后面的章节中可以了解更多关于这些概念的信息。

  • 在 TypeScript 版本 4.2 之前,类型别名可能会出现在错误消息中,有时会代替等效的匿名类型。接口的名称将始终在错误消息中。
  • 类型别名不参与声明合并,但接口可以。

大多数情况下,您可以根据个人喜好进行选择,TypeScript 会告诉您是否需要另一种声明。如果您想要启发式,请优先使用  interface  直到您需要使用  type  中的功能。

类型断言

有时你比 TypeScript 更了解值的类型信息。

例如,如果您使用  document.getElementById ,TypeScript 只知道这将返回某种  HTMLElement ,但您知道这是一个 Canvas 元素,所以类型是  HTMLCanvasElement 。

在这种情况下,您可以使用类型断言来指定更具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

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

您还可以使用尖括号语法来表示类型断言(但在  .tsx  文件中不能这样,否则会语法报错)

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

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

const x = "hello" as number;
// 将类型“string”转换为类型“number”可能是一个错误,因为两种类型都没有充分重叠。
// 如果这是故意的,请先将表达式转换为“unknown”。

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

const a = (expr as any) as T;

字面量类型

除了一般类型  string  和  number ,我们还可以在类型位置引用特定的字符串和数字。

考虑到字面量类型的原因是:var  和  let  允许更改变量中保存的内容,而  const  不允许。那么 const 定义的变量类型就是字面量类型的。

let changingString = "Hello World";
changingString = "Olá Mundo";
// 他的类型是 string,因为 changinString 可以代表任何可能的字符串
// 这也是TypeScript在类型系统中描述它的方式
changingString;

const constantString = "Hello World";
// 因为 constantString 只能代表这一种字符串,即 "Hello World"
// 所以它的类型是字面量 "Hello World"
constantString;

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

image.png

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

但是通过将字面量类型联合,您可以表达一个更有用的概念:例如,只接受一组特定已知值的函数:

image.png

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

image.png

当然,还可以将这些类型与非字面量类型结合起来:

image.png

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

字面量推理

当您使用对象初始化变量时,TypeScript 假定该对象的属性可能会更改。例如:

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

TypeScript 不会认为 counter 属性是字面量类型  0,因为属性是可以读取和写入的,所以 counter 属性的类型是 number

这种规则同样适用于字符串:

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 之间更改 method 属性,这可以将新字符串(如 "GUESS" 分配给 req.method ),所以 TypeScript 认为此代码有错误。

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

  1. 可以通过在任一位置添加类型断言来更改推断:
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");

Change 1 表示“我打算让 req.method 始终具有文字类型 "GET" ”,从而防止之后可能将 "GUESS" 分配给该字段。 Change 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  选项。

strictNullChecksoff

关闭  strictNullChecks  后,仍然可以正常访问可能为  null  或  undefined  的值,并且可以将值  null  和  undefined  分配给任何类型的属性。这类似于没有空值检查的语言(例如 C#、Java)的行为方式。缺乏对这些值的检查往往是错误的主要来源;所以如果代码允许,我们总是建议打开  strictNullChecks  配置项。

strictNullCheckson

在打开  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) {
  // No error
  console.log(x!.toFixed());
}

就像其他类型断言一样,这不会改变代码的运行时行为,因此一定要当您知道值不是  null  或  undefined  时,才只使用  !

枚举

枚举是 TypeScript 添加到 JavaScript 的一项新功能,它允许描述一个值,该值可以是一组可能的命名常量中的一个。与大多数 TypeScript 功能不同,这不是对 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 ,用于通过函数  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.
}