JavaScript 现在是有史以来最广泛使用的跨平台语言之一。JavaScript 最初是一种用于向网页添加微不足道的交互性的小型脚本语言,现已发展成为各种规模的前端和后端应用的首选语言。 虽然用 JavaScript 编写的程序的大小、作用域和复杂性呈指数级增长,但 JavaScript 语言表达不同代码单元之间关系的能力却没有。 结合 JavaScript 相当特殊的运行时语义,语言和程序复杂性之间的这种不匹配使得 JavaScript 开发成为一项难以大规模管理的任务。
程序员编写的最常见的错误类型可以描述为类型错误: 在期望不同类型的值的地方使用了某种类型的值。 这可能是由于简单的拼写错误、未能理解库的 API 表面、对运行时行为的错误假设或其他错误。 TypeScript 的目标是成为 JavaScript 程序的静态类型检查器 - 换句话说,一个在代码运行之前运行的工具(静态)并确保程序的类型正确(类型检查)。
TypeScript 是一种基于 JavaScript 构建的强类型编程语言,可为你提供任何规模的更好工具。如果没有 JavaScript 背景的情况下使用 TypeScript,并且打算将 TypeScript 作为你的第一语言,建议首先开始阅读有关 Microsoft Learn JavaScript 教程 (https://developer.microsoft.com/zh-cn/javascript/) 或阅读 Mozilla 网络文档中的 JavaScript (https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide) 的文档。
TypeScript 可以通过三种安装路径进行安装,具体取决于你打算如何使用它:npm 模块、NuGet 包、或者 Visual Studio 插件。
本文介绍在 Node/npm 环境下安装 TypeScript 和 TypeScript 常用类型。
TypeScript:https://www.typescriptlang.org/(中文版:https://ts.nodejs.cn/)
Microsoft TypeScript: https://devblogs.microsoft.com/typescript/
GitHub:https://github.com/microsoft/TypeScript
NPMJS: https://www.npmjs.com/package/typescript
1. 系统环境
操作系统:Windows 10 (x64)
NVM:1.1.11
NodeJS: 14.21.3 LTS
NPM:6.14.18
Typescript: 5.2.2
工作目录:D:\workshop\nodejs
2. 安装 TypeScript
1) 创建 npm 项目
进入工作目录 D:\workshop\nodejs,手动创建子目录 tsdemo,在命令行控制台进入该子目录,运行如下命令:
D:\workshop\nodejs\tsdemo> npm init
...
注:可以运行带 -y 参数的命令 npm init -y,即出现交互选择时都自动选 yes。init 命令运行完成后,tsdemo 目录先生成一个 package.json 文件。
2) 下载 TypeScript 到 npm 项目
把 TypeScript 模块写入 devDependencies 节点,npm install 初始化时会自动下载模块。
D:\workshop\nodejs\tsdemo> npm install typescript --save-dev
...
或把 TypeScript 模块写入 dependencies 节点,npm install 初始化时会自动下载模块。
D:\workshop\nodejs\tsdemo> npm install typescript --save
...
或把 TypeScript 模块写入 dependencies 节点,npm install 初始化时不会自动下载模块,需要手动下载。
D:\workshop\nodejs\tsdemo> npm install typescript
...
3) 示例
在 D:\workshop\nodejs\tsdemo 目录下创建 greeter.ts 文件,代码如下:
function greeter(person) { return "Hello, " + person; } let user = "Jane User"; if (typeof(document) === 'undefined') { console.log(greeter(user)); } else { document.body.textContent = greeter(user); }
注: 在 Visual Studio 中打开 greeter.ts,可以将鼠标悬停在标识符(变量、函数名等)上以查看它们的类型。请注意,在某些情况下,这些类型是自动为你推断的。重新输入最后一行,并根据 DOM 元素的类型查看完成列表和参数帮助。将光标放在对 greeter 函数的引用上,然后按 F12 转到它的定义。还要注意,可以右键单击一个符号并使用重构来重命名它。
运行 npx tsc 命令编译 greeter.ts 文件:
D:\workshop\nodejs\tsdemo> npx tsc greeter.ts
注:在同级目录下,生成了一个 greeter.js 文件。
命令行方式运行 greeter.js,格式如下:
D:\workshop\nodejs\tsdemo> node greeter.js
Hello, Jane User
浏览器方式运行 greeter.js,这里创建一个 greeter.html 文件,代码如下:
<!DOCTYPE html> <html> <head> <title>TypeScript Greeter</title> </head> <body> <script src="greeter.js"></script> </body> </html>
在浏览器中打开 greeter.html 文件,显示和命令行运行一样的内容。
4) 安装 ts-node
命令行直接执行 ts 文件,需要安装 ts-node,安装命令如下:
D:\workshop\nodejs\tsdemo> npm install -g ts-node
上文的 greeter.ts 文件,其实是一个 Javascript 格式的文件,不符合 TypeScript 的类型要求,需要改成如下格式:
function greeter(person: string) { return "Hello, " + person; } let user: string = "Jane User"; console.log(greeter(user));
命令行方式运行 greeter.ts,格式如下:
D:\workshop\nodejs\tsdemo> ts-node greeter.ts
Hello, Jane User
3. TypeScript 常用类型
JavaScript 有三个常用的基本类型: string、number 和 boolean。在 TypeScript 中都有对应的类型,可以使用 JavaScript typeof 运算符查看:
string 表示字符串值,如 "Hello, world"
number 代表像 12 这样的数字。 JavaScript 对整数没有特殊的运行时值,因此没有等价于 int 或 float - 一切都只是 number
boolean 代表 true 和 false 这两个值
类型名称 String、Number 和 Boolean(以大写字母开头)是合法的,但指的是一些很少出现在代码中的特殊内置类型。一般使用 string、number 或 boolean 作为类型。
TypeScript 新增或不同于 JavaScript 的类型或类型概念:类型注解 (Type Annotation)、联合类型 (Union Type)、类型别名 (Type Alias)、接口 (Interface)、类型断言 (Type Assertion)、字面类型 (Literal Type)、字面推断 (Literal Inference) 等。
1) 类型注解 (Type Annotation)
使用 const、var 或 let 声明变量时,可以选择添加类型注解以显式指定变量的类型。声明函数时,可以在每个参数后面加上类型注解,声明函数接受哪些类型的参数,也可以给函数添加返回类型注解。示例代码如下:
let myName: string = "Alice"; // 变量的类型注解 // 函数参数和返回值的类型注解 function greet(name: string): boolean { console.log("Hello, " + name.toUpperCase() + "!"); return true; } console.log(greet(myName));
注:在大多数情况下,不需要加类型注解。TypeScript 会尽可能地尝试自动推断代码中的类型。
匿名函数与函数声明有些不同。当一个匿名函数出现在 TypeScript 可以确定如何调用它的地方时,该函数的参数会自动被赋予类型。示例如下:
const names = ["Alice", "Bob", "Eve"]; // 参数 s 不需要类型注解, 使用箭头函数或一般函数效果一样 names.forEach((s) => { console.log(s.toUpperCase()); });
2) 联合类型 (Union Type)
联合类型是由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值,这些类型中的每一种被称为联合类型的成员。
使用 const、var 或 let 声明联合类型变量,格式如下:
let id: number | string = 5; console.log(typeof(id)); // number id = 'abc'; console.log(typeof(id)); // string id = { name: 'typescript' }; console.log(typeof(id)); // object
注:以上代码运行 npx tsc 命令时,编译系统会提示参数类型错误,因为 { name: 'typescript' } 是 Javascript 对象,不是 number 也不是 string,报错但不会影响在同级目录生成 js 文件。
带联合类型参数的函数,格式如下:
function printId(id: number | string) { if (typeof id === "string") { // In this branch, id is of type 'string' console.log("Your ID is: " + id.toUpperCase()); } else { // Here, id is of type 'number' console.log("Your ID is: " + id); } } printId(100); // Your ID is: 100 printId('xyz'); // Your ID is: XYZ printId({ myID: 123 }); // Your ID is: [object Object]
注:以上代码运行 npx tsc 命令时,编译系统会提示参数类型错误,因为 { myID: 123 } 是 Javascript 对象,不是 number 也不是 string,报错但不会影响在同级目录生成 js 文件。
3) 类型别名 (Type Alias)
通常我们在类型注解中编写对象类型或联合类型,它们一般有多个成员,比如上文的 number | string。这些对象类型或联合类型其实是我们的自定义类型,在同一个 ts 文件中可能会被重复使用,而且有时它们的成员数量可能比较多。显然,这会带来代码冗余、修改时需要同时修改多处代码等问题。
于是,给我们的自定义类型取一个简短好记的别名,在某些情况下是一件很有用甚至必要的事情。
基本类型的别名,格式如下:
type stringType = string; let str: stringType = 'basic'; console.log(typeof(str)); // string
注:在类型注解里使用 stringType 别名,相当于使用 string,有点类似于语法糖,实际作用不大,所以基本类型一般很少使用别名。
联合类型的别名,格式如下:
type ID = number | string; let id1: ID = 101; console.log(typeof(id1)); // number let id2: ID = 'aaa'; console.log(typeof(id2)); // string
对象类型的别名,格式如下:
type Point = { x: number; y: number; }; // Exactly the same as the earlier example function printCoord(pt: Point) { console.log("type:x = " + pt.x); console.log("type:y = " + pt.y); } printCoord({ x: 100, y: 100 });
4) 接口 (Interface)
接口(interface)是命名对象类型的另一种方式,格式如下:
interface Point2 { x: number; y: number; } function printCoord2(pt: Point2) { console.log("interface: x = " + pt.x); console.log("interface: y = " + pt.y); } printCoord2({ x: 200, y: 200 });
就像我们在上面使用类型别名时一样,该示例就像我们使用匿名对象类型一样工作。TypeScript 只关心我们传递给 printCoord 的值的结构 - 它只关心它是否具有预期的属性。 只关心类型的结构和功能是我们称 TypeScript 为结构类型类型系统的原因。
接口和类型别名非常相似,在很多情况下可以在它们之间自由选择。接口的几乎所有功能都在类型别名中可用,主要区别在于类型别名无法重新打开类型以添加新属性,而接口始终可以扩展。
5) 类型断言 (Type Assertion)
使用类型断言来指定更具体的类型,格式如下:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
注:document.getElementById,TypeScript 只知道这将返回某种 HTMLElement,这里使用 as 来预先判断它是 HTMLCanvasElement 。
与类型注解一样,类型断言被编译器删除,不会影响代码的运行时行为。还可以使用尖括号语法(在 .tsx 文件中),格式如下:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
注:因为类型断言在编译时被删除,所以没有与类型断言关联的运行时检查。 如果类型断言错误,则不会产生异常或 null。
TypeScript 只允许类型断言转换为更具体或更不具体的类型版本。此规则可防止 “impossible” 强制,例如:
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.
有时,此规则可能过于保守,并且不允许可能有效的更复杂的强制转换。 如果发生这种情况,你可以使用两个断言,首先是 any(或 unknown,我们稍后会介绍),然后是所需的类型:
const a = expr as any as T;
6) 字面类型 (Literal Type)
除了通用类型 string 和 number 之外,我们还可以在类型位置引用特定的字符串和数字。
考虑这一点的一种方法是考虑 JavaScript 如何使用不同的方法来声明变量。 var 和 let 都允许更改变量中保存的内容,而 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; // 它的类型注解是 string const constantString = "Hello World"; // Because `constantString` can zhejieonly represent 1 possible string, it // has a literal type representation constantString; // 它的类型注解是 "Hello World","Hello World" 成了字面类型
就其本身而言,字面类型并不是很有价值:
let x: "hello" = "hello"; // OK x = "hello"; // ... x = "howdy"; // Type '"howdy"' is not assignable to type '"hello"'.
变量只能有一个值并没有多大用处。但是通过将字面组合成联合,你可以表达一个更有用的概念 - 例如,只接受一组已知值的函数:
function printText(s: string, alignment: "left" | "right" | "center") { console.log(s + ' -> ' + alignment); } printText("Hello, world", "left"); printText("G'day, mate", "centre"); // Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
数字字面类型的工作方式相同:
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") { console.log(x); } configure({ width: 100 }); configure({ height: 200 }); // Argument of type '{ height: number; }' is not assignable to parameter of type '"auto" | Options'. configure("auto"); configure("automatic"); // Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
还有一种字面类型: 布尔字面量。 只有两种布尔字面类型,正如你可能猜到的,它们是 true 和 false 类型。类型 boolean 本身实际上只是联合 true | false 的别名。
7) 字面推断 (Literal Inference)
当你使用对象初始化变量时,TypeScript 假定该对象的属性可能会在以后更改值。例如,如果你编写如下代码:
const obj = { counter: 0 }; if (someCondition) { obj.counter = 1; }
TypeScript 不假定将 1 分配给先前具有 0 的字段是错误的。 另一种说法是 obj.counter 必须具有 number 类型,而不是 0,因为类型用于确定读取和写入行为。
这同样适用于字符串:
function handleRequest(url: string, method: "GET" | "POST"): void { console.log(method + ' -> ' + url); } 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 的调用之间评估代码,这可以将一个新的字符串(如 "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");
更改 1 表示 “我打算让 req.method 始终具有 _ 字面类型 _"GET"“,防止之后可能将 "GUESS" 分配给该字段。 更改 2 表示 “由于其他原因,我知道 req.method 的值为 "GET"“。
(2) 可以使用 as const 将整个对象转换为字面类型:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
as const 后缀的作用类似于 const,但用于类型系统,确保为所有属性分配字面类型,而不是更通用的版本,如 string 或 number。
8) 其它
(1) 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。不过,通常希望避免这种情况,因为 any 没有经过类型检查。可以使用编译器标志 noImplicitAny 将任何隐式 any 标记为错误。
(2) null 和 undefined
JavaScript 有两个原始值用于表示值不存在或未初始化的值: null 和 undefined。TypeScript 有两个对应的同名类型。这些类型的行为取决于你是否启用了 strictNullChecks 选项:
strictNullChecks 关闭: 可能是 null 或 undefined 的值仍然可以正常访问,并且值 null 和 undefined 可以分配给任何类型的属性。 这类似于没有空检查的语言(例如 C#、Java)的行为方式。 缺乏对这些值的检查往往是错误的主要来源; 如果在他们的代码库中这样做是可行的,我们总是建议人们打开 strictNullChecks。
strictNullChecks 开启: 当值为 null 或 undefined 时,你需要在对该值使用方法或属性之前测试这些值。
就像在使用可选属性之前检查 undefined 一样,我们可以使用缩小来检查可能是 null 的值:
function doSomething(x: string | null) { if (x === null) { // do nothing } else { console.log("Hello, " + x.toUpperCase()); } }
(3) 非空断言运算符(后缀!)
TypeScript 还具有一种特殊的语法,可以在不进行任何显式检查的情况下从类型中删除 null 和 undefined,即忽略 null 或 undefined 的空值警告。
function liveDangerously(x: number | null) { // No error console.log(x!.toFixed()); }
需要谨慎使用 !,因为它绕过了 TypeScript 的类型检查,如果 x 实际值为 null 或 undefined 时,
(4) 枚举
枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。 与大多数 TypeScript 功能不同,这不是对 JavaScript 的类型级添加,而是添加到语言和运行时的东西。 正因为如此,这是一个你应该知道存在的功能,但除非你确定,否则可能会推迟使用。 你可以在 枚举参考页 中阅读有关枚举的更多信息。
(5) 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 的更多信息。
(6) symbol
JavaScript 中有一个原语用于通过函数 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. // Can't ever happen }