- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
导读:本文基于 TypeScript 使用手册(中文版)仓库的 TypeScript 1.8 版本发布说明,系统梳理该版本引入的类型系统能力(F-Bounded 多态、控制流分析、字符串字面量类型、基于
this的类型收窄)、模块与 JSX 支持改进,以及编译器命令行与tsconfig.json的工具链升级。读完本文,你将掌握 1.8 中每个新特性的语法形态、编译选项开关(含默认值与禁用方式),并能结合实际代码理解其底层行为。
TypeScript 1.8 是语言与工具链双线并进的一个重要版本:类型层面引入了控制流分析、字符串字面量类型、基于this的类型收窄与模块扩充;工具链层面则允许.js文件直接编译、支持tsconfig.json注释、--project指向任意配置文件,并让模块输出统一带上"use strict"。本文逐节还原官方发布说明的完整内容,并结合仓库中的编译选项清单、破坏性改动记录等文档做深度佐证。
类型参数约束:F-Bounded 多态
在 TypeScript 1.8 之前,类型参数的限制(extends约束)不能引用同一个类型参数列表中的其他类型参数,编译器会直接报错。1.8 解除了这一限制:类型参数可以引用来自同一列表中的兄弟类型参数,这种特性在类型系统理论中被称为F-Bounded Polymorphism(F 有界多态,参见有界量化理论)。
官方给出的示例是一个通用的属性合并函数:
function assign<T extends U, U>(target: T, source: U): T { for (let id in source) { target[id] = source[id]; } return target; } let x = { a: 1, b: 2, c: 3, d: 4 }; assign(x, { b: 10, d: 20 }); assign(x, { e: 0 }); // 错误这里T extends U表示:T必须是U的子类型。调用assign(x, { e: 0 })时,编译器推导T = { a: number; b: number; c: number; d: number }、U = { e: number },由于{ e: number }并不是{ a...d }的属性子集,约束失败而报错——这正是"参数约束可以引用兄弟参数"带来的类型检查能力。泛型与约束的基础知识可进一步参考手册的泛型章节。
控制流错误分析
TypeScript 1.8 引入了控制流分析(Control Flow Analysis)来捕获开发者常见的编码错误。该能力覆盖四类检查,其中两类默认开启、两类默认关闭,均可通过编译选项控制(详见 compiler-options.md 中--allowUnreachableCode、--allowUnusedLabels、--noImplicitReturns、--noFallthroughCasesInSwitch四行的说明):
| 检查项 | 默认状态 | 相关编译选项 |
|---|---|---|
| 不可及代码(Unreachable Code) | 默认开启 | --allowUnreachableCode禁用 |
| 未使用的标签(Unused Labels) | 默认开启 | --allowUnusedLabels禁用 |
| 隐式返回(Implicit Returns) | 默认关闭 | --noImplicitReturns启用 |
| Case 语句贯穿(Fallthrough) | 默认关闭 | --noFallthroughCasesInSwitch启用 |
不可及的代码
一定无法在运行时执行的语句会被标记"代码不可及"错误。典型场景是无条件限制的return、throw、break或continue之后的语句:
function f(x) { if (x) { return true; } else { return false; } x = 0; // 错误: 检测到不可及的代码. }这个特性还能捕获一个非常常见的真实 bug——在return后误加换行。由于 JavaScript 的自动分号插入(ASI)会在行末结束return语句,下面的对象字面量变成了一个无法到达的代码块:
function f() { return; // 换行导致自动插入的分号 { x: 'string'; // 错误: 检测到不可及的代码. } }未使用的标签
未使用的标签同样会被标记,例如声明了loop:却从未通过break loop/continue loop引用:
loop: while (x > 0) { // 错误: 未使用的标签. x++; }注意这条检查与不可及代码一样默认开启,可通过--allowUnusedLabels关闭。在 1.8 破坏性改动记录 中还给出了另一个更隐蔽的例子:箭头函数中的对象字面量(x) => { x: x }里,x:会被解析为标签而不是属性,从而触发未使用标签报错——这正是该检查能拦截的"无返回值分支里隐式对象返回"问题。
隐式返回
JavaScript 中,没有返回值的代码分支会隐式返回undefined。1.8 起编译器可以将这种情况标记为隐式返回,但该检查默认禁用,需要--noImplicitReturns启用:
function f(x) { // 错误: 不是所有分支都返回了值. if (x) { return false; } // 隐式返回了 `undefined` }Case 语句贯穿
TypeScript 现在可以在switch语句中,对贯穿了几个非空 case的情况报错。该检测默认关闭,通过--noFallthroughCasesInSwitch启用:
switch (x % 2) { case 0: // 错误: switch 中出现了贯穿的 case. console.log('even'); case 1: console.log('odd'); break; }而空 case 的贯穿是合法的(多个 case 共享同一段执行体,这是常见写法),不会报错:
switch (x % 3) { case 0: case 1: console.log('Acceptable'); break; case 2: console.log('This is *two much*!'); break; }关于这四类检查的启用/禁用细节,破坏性改动文档还提示:如果报错但你认为代码本身合理,可以通过对应编译选项阻止报错,例如将allowUnreachableCode设为true。
React 函数组件
TypeScript 1.8 开始支持 React 的函数组件(Functional Components)——一种可以组合其他组件的轻量级组件。使用参数解构和默认值即可轻松定义props的类型,且组件参数可以被类型系统检验:
// 使用参数解构和默认值轻松地定义 'props' 的类型 const Greeter = ({ name = 'world' }) => <div>Hello, {name}!</div>; // 参数可以被检验 let example = <Greeter name="TypeScript 1.8" />;需要说明的是,使用这一特性需要配合最新版的react.d.ts(DefinitelyTyped 仓库维护)。JSX 的完整类型规则可参考手册的 JSX 章节。
简化的 Reactprops类型管理
配合最新的react.d.ts,1.8 大幅简化了props的类型声明:
- 不再需要显式声明
ref和key,也不必再extend React.Props; ref和key属性会在所有组件上拥有正确的类型;ref属性在无状态函数组件(即函数组件)上会被正确地禁用。
这让 JSX 使用体验更接近原生 HTML 元素的属性检查。
在模块中扩充全局或模块作用域
1.8 允许用户对任何模块进行扩充(Augmentation),扩充的形式与过去的外部包模块声明一致(例如declare module "foo" { }这种语法),并且可以直接嵌在自己的模块内,或放在另外一个顶级外部包模块中。此外,TypeScript 还以declare global { }的形式提供了对全局声明的扩充,使模块在必要时可以对Array这样的全局类型进行补充。
模块扩充的名称解析规则与import、export声明一致;扩充的模块声明合并方式与在同一个文件中声明相同。一个重要的约束是:不论是模块扩充还是全局声明扩充,都不能向顶级作用域添加新的项目——它们只能为已经存在的声明添加"补丁"。声明合并的底层机制(接口合并、命名空间合并等)详见声明合并文档。
模块扩充示例
下面map.ts声明它会在内部修改observable.ts中声明的Observable类型,为其添加map方法:
// observable.ts export class Observable<T> { // ... }// map.ts import { Observable } from "./observable"; // 扩充 "./observable" declare module "./observable" { // 使用接口合并扩充 'Observable' 类的定义 interface Observable<T> { map<U>(proj: (el: T) => U): Observable<U>; } } Observable.prototype.map = /*...*/;// consumer.ts import { Observable } from './observable'; import './map'; let o: Observable<number>; o.map(x => x.toFixed());注意第 2.7 版本的发布说明(typescript-2.7.md)中,map方法内部的实现现在会要求做更多类型层面的细化,但 1.8 确立的"在消费侧通过declare module补丁已有模块类型"的模式一直沿用至今。
全局作用域扩充示例
相似地,在模块中可以通过declare global增强全局作用域:
// 确保当前文件被当做一个模块. export {}; declare global { interface Array<T> { mapToNumbers(): number[]; } } Array.prototype.mapToNumbers = function () { /* ... */ };注意开头的export {};——只有文件是模块时才能使用declare global。
字符串字面量类型
接受一个特定字符串集合作为某个值的 API 并不少见。考虑一个可以控制动画渐变(inbetweening)让元素在屏幕中滑动的 UI 库,传统写法容易产生错误:当用户不小心拼错一个合法的值时,没有任何提示:
declare class UIElement { animate(options: AnimationOptions): void; } interface AnimationOptions { deltaX: number; deltaY: number; easing: string; // 可以是 "ease-in", "ease-out", "ease-in-out" } // 没有报错 new UIElement().animate({ deltaX: 100, deltaY: 100, easing: 'ease-inout' });在 TypeScript 1.8 中新增了字符串字面量类型:这些类型与字符串字面量的写法一致,只是写在类型的位置。用户现在可以确保类型系统捕获拼写错误:
interface AnimationOptions { deltaX: number; deltaY: number; easing: 'ease-in' | 'ease-out' | 'ease-in-out'; } // 错误: 类型 '"ease-inout"' 不能复制给类型 '"ease-in" | "ease-out" | "ease-in-out"' new UIElement().animate({ deltaX: 100, deltaY: 100, easing: 'ease-inout' });字符串字面量类型如今已是 TypeScript 的核心能力,其使用方式(联合、类型别名、函数重载)在手册的字面量类型章节中有系统讲解;此外还有数字字面量类型与布尔字面量类型,例如function rollDice(): 1 | 2 | 3 | 4 | 5 | 6。
更好的联合/交叉类型接口
TypeScript 1.8 优化了源类型和目标类型都是联合或交叉类型时的类型推导。举例来说,当从string | string[]推导到string | T时,编译器会将类型拆解为string[]和T,从而把string[]推导为T。这一改进让类型守卫与泛型工具函数的组合推导更加精确:
type Maybe<T> = T | void; function isDefined<T>(x: Maybe<T>): x is T { return x !== undefined && x !== null; } function isUndefined<T>(x: Maybe<T>): x is void { return x === undefined || x === null; } function getOrElse<T>(x: Maybe<T>, defaultValue: T): T { return isDefined(x) ? x : defaultValue; } function test1(x: Maybe<string>) { let x1 = getOrElse(x, 'Undefined'); // string let x2 = isDefined(x) ? x : 'Undefined'; // string let x3 = isUndefined(x) ? 'Undefined' : x; // string } function test2(x: Maybe<number>) { let x1 = getOrElse(x, -1); // number let x2 = isDefined(x) ? x : -1; // number let x3 = isUndefined(x) ? -1 : x; // number }上例中的x is T是类型谓词(type predicate),即用户定义的类型守卫函数(User-Defined Type Guards),其完整规则见高级类型文档。
使用--outFile合并 AMD 和 System 模块
在 1.8 中,使用--module amd或--module system的同时指定--outFile,会把所有参与编译的模块合并为单个包含多个模块闭包的输出文件。每一个模块都会根据其相对于rootDir的位置计算出自己的模块名称。
示例源码(两个文件):
// 文件 src/a.ts import * as B from './lib/b'; export function createA() { return B.createB(); }// 文件 src/lib/b.ts export function createB() { return {}; }合并结果为:
define('lib/b', ['require', 'exports'], function (require, exports) { 'use strict'; function createB() { return {}; } exports.createB = createB; }); define('a', ['require', 'exports', 'lib/b'], function (require, exports, B) { 'use strict'; function createA() { return B.createB(); } exports.createA = createA; });可见输出中以模块相对rootDir的位置(lib/b、a)为各闭包命名。需要提醒的是,编译选项文档明确:只有"AMD"和"System"能和--outFile一起使用;而 1.8 的破坏性改动也提到,其他--module值搭配--outFile时此前会静默生成空输出文件,现在会直接报错(详见 typescript-1.8 破坏性改动)。
支持 SystemJS 使用default导入
像 SystemJS 这样的模块加载器将 CommonJS 模块做了包装并暴露为defaultES6 导入项。这导致 SystemJS 与 CommonJS 两种实现因为加载器模块导出方式不同,无法共享定义。
设置新的编译选项--allowSyntheticDefaultImports,可以指明模块加载器会进行导入的.ts或.d.ts中未指定的某种类型的默认导入项构建;编译器会由此推断存在一个default导出项,并且整个模块自身与它一致。此选项在 System 模块下默认开启。编译选项文档还补充了它的默认值细节:module === "system"或设置了--esModuleInterop时为默认开启,且该选项仅影响类型检查、不影响代码输出。
允许循环中被引用的let/const
此前在循环中创建闭包引用let/const会报错,TypeScript 1.8 起支持这一写法,并且循环中被函数引用的let/const声明会被输出为与let/const更新语义相符的代码:
let list = []; for (let i = 0; i < 5; i++) { list.push(() => i); } list.forEach(f => console.log(f()));编译输出(_loop_1将每次迭代的i作为参数传入,保证每个闭包捕获各自迭代的值):
var list = []; var _loop_1 = function (i) { list.push(function () { return i; }); }; for (var i = 0; i < 5; i++) { _loop_1(i); } list.forEach(function (f) { return console.log(f()); });运行结果是:
0 1 2 3 4这修正了经典 JavaScript 闭包陷阱(所有闭包共享同一个i),让 TypeScript 输出与 ES6 语义保持一致。
改进的for..in语句检查
过去for..in变量的类型被推断为any,导致编译器忽略语句内一些不合法的使用。从 TypeScript 1.8 开始:
for..in语句中的变量隐含类型为string;- 当一个有数字索引签名且类型为
T的对象(比如数组)被for..in的变量索引时,产生值的类型为T。
var a: MyObject[]; for (var x in a) { // x 的隐含类型为 string var obj = a[x]; // obj 的类型为 MyObject }由于x的类型是string而数组恰好有数字索引签名,a[x]仍能正确索引出MyObject——类型信息从此不再丢失。
模块输出统一加"use strict"
对 ES6 来说模块始终以严格模式被解析,但这一点过去对于非 ES6 目标在生成的代码中并没有遵循。从 TypeScript 1.8 开始,输出的模块总会处于严格模式。由于多数严格模式下的错误同时也是 TypeScript 编译时的错误,大多数代码并不会有可见改动;但一些东西可能在运行时没有征兆地失败,比如赋值给NaN现在会有运行时错误。
该改动属于破坏性变更(见 breaking-changes/typescript-1.8.md),若想禁用此行为,可在命令行传入--noImplicitUseStrict或在tsconfig.json中指定。同一版本的破坏性改动还包括:从模块导出非局部名称(如export { Promise })会按 ES6 规范报错,需先用局部变量捕获再导出。
使用--allowJs加入.js文件
项目中经常存在外部非 TypeScript 编写的源文件。一种做法是把 JS 代码转换为 TS 代码,但又希望把所有 JS 代码和新的 TS 代码的输出一起打包为一个文件。
.js文件现在允许作为tsc的输入文件:TypeScript 编译器会检查.js输入文件的语法错误,并根据--target和--module选项输出对应代码,输出也会和其他.ts文件放在一起;.js文件的 source maps 也会像.ts文件一样被生成。编译选项文档补充说明该选项默认值为false(即默认不编译.js),且可配合--checkJs在.js文件中报告类型错误、配合--maxNodeModuleJsDepth控制node_modules中 JavaScript 文件的加载深度。
使用--reactNamespace自定义 JSX 工厂
在使用--jsx react的同时使用--reactNamespace <JSX 工厂名称>,可以指定一个不同于默认React的 JSX 工厂。新的工厂名称会被用来调用createElement和__spread方法。
import { jsxFactory } from 'jsxFactory'; var div = <div>Hello JSX!</div>;编译参数:
tsc --jsx react --reactNamespace jsxFactory --m commonJS结果:
'use strict'; var jsxFactory_1 = require('jsxFactory'); var div = jsxFactory_1.jsxFactory.createElement('div', null, 'Hello JSX!');编译选项文档中该选项默认值为"React"。后来 TypeScript 又引入了更通用的--jsxFactory选项(默认"React.createElement"),但 1.8 的--reactNamespace是最早的自定义工厂方案。
基于this的类型收窄
TypeScript 1.8 为类和接口方法扩展了用户定义的类型收窄函数(类型守卫):this is T现在可以是类或接口方法的合法返回值类型标注。当在类型收窄的位置使用(比如if语句)时,函数调用表达式的目标对象类型会被收窄为T。
class FileSystemObject { isFile(): this is File { return this instanceof File; } isDirectory(): this is Directory { return this instanceof Directory; } isNetworked(): this is Networked & this { return this.networked; } constructor(public path: string, private networked: boolean) {} } class File extends FileSystemObject { constructor(path: string, public content: string) { super(path, false); } } class Directory extends FileSystemObject { children: FileSystemObject[]; } interface Networked { host: string; } let fso: FileSystemObject = new File('foo/bar.txt', 'foo'); if (fso.isFile()) { fso.content; // fso 是 File } else if (fso.isDirectory()) { fso.children; // fso 是 Directory } else if (fso.isNetworked()) { fso.host; // fso 是 networked }注意isNetworked(): this is Networked & this是交叉类型与this谓词的组合:收窄后fso同时具备Networked(host)和this自身的成员。该机制是后续各版本"基于控制流的类型收窄"的基础,更完整的类型守卫(parameterName is Type形式的谓词函数)规则参见高级类型文档,以及手册 v2 的收窄(Narrowing)章节。
官方 TypeScript NuGet 包
从 TypeScript 1.8 开始,官方提供 TypeScript 编译器(tsc.exe)和 MSBuild 整合(Microsoft.TypeScript.targets与Microsoft.TypeScript.Tasks.dll)的 NuGet 包。稳定版包括Microsoft.TypeScript.Compiler与Microsoft.TypeScript.MSBuild;与每日 npm 包对应的每日 NuGet 包则托管在 myget.org 的TypeScript-Preview源中。该能力为 .NET 生态(如 ASP.NET 项目)的构建接入提供了官方途径,仓库中在 MSBuild 里使用编译选项一章详细说明了 MSBuild 场景下的配置方式。
tsc错误信息更美观(--pretty)
大量单色的输出并不直观。颜色可以帮助识别信息的始末,这些视觉线索在处理复杂错误信息时非常重要。通过传递--pretty命令行选项,TypeScript 会给出更丰富的输出,包含错误发生的上下文(高亮、上下文代码片段)。编译选项文档中该选项默认值为false。
高亮 VS 2015 中的 JSX 代码
在 TypeScript 1.8 中,JSX 标签可以在 Visual Studio 2015 中被分别和高亮。通过工具→选项→环境→字体与颜色页面,在VB XML颜色和字体设置中还可以进一步自定义字体和颜色。
--project(-p)选项接受任意文件路径
--project命令行选项过去只接受包含tsconfig.json文件的文件夹。考虑到不同的构建场景,1.8 起允许--project指向任何兼容的 JSON 文件。比如用户可能希望为 Node 5 编译 CommonJS 的 ES2015、为浏览器编译 AMD 的 ES5,少了这项限制后可以更容易地直接用tsc管理不同的构建目标,无需再通过"把多个tsconfig.json放在不同目录"这类迂回方式。如果参数是一个目录路径,行为保持不变——编译器仍会尝试在该目录下寻找名为tsconfig.json的文件。tsconfig.json的完整管理方式见工程配置文档。
允许 tsconfig.json 中的注释
为配置添加文档是很棒的!tsconfig.json现在支持单行和多行注释:
{ "compilerOptions": { "target": "ES2015", // 跑在 node v5 上, 呀! "sourceMap": true // 让调试轻松一些 }, /* * 排除的文件 */ "exclude": [ "file.d.ts" ] }在此之前 JSON 严格禁止注释,这一改动让配置文件第一次具备了内联文档能力。
支持输出到 IPC 驱动的文件
TypeScript 1.8 允许将--outFile参数与一些特殊的文件系统对象一起使用,比如命名管道(pipe)、设备(devices)等。例如在很多与 Unix 相似的系统上,标准输出流可以通过文件/dev/stdout访问:
tsc foo.ts --outFile /dev/stdout这一特性也允许把输出交给其他命令处理,比如将生成的 JavaScript 管道给格式美化工具:
tsc foo.ts --outFile /dev/stdout | pretty-js改进了 Visual Studio 2015 中对 tsconfig.json 的支持
TypeScript 1.8 允许在任何种类的项目中使用tsconfig.json文件,包括 ASP.NET v4 项目、控制台应用以及用 TypeScript 开发的 HTML 应用。同时可以添加不止一个tsconfig.json文件,其中每一个都会作为项目的一部分被构建——这使得可以在不使用多个不同项目的情况下,为应用的不同部分使用不同配置。当项目中添加了tsconfig.json文件时,项目属性页面会被禁用,所有配置的改变必须在tsconfig.json中进行。
官方文档同时给出了一些限制:
- 如果添加了一个
tsconfig.json文件,不在其上下文中的 TypeScript 文件不会被编译; - Apache Cordova 应用依然有单个
tsconfig.json文件的限制,且该文件必须在根目录或scripts文件夹; - 多数项目类型中都没有
tsconfig.json的模板。
小结
TypeScript 1.8 是一个承前启后的版本:它把控制流分析带入了编译器(不可及代码、未使用标签、隐式返回、case 贯穿四类检查),确立了字符串字面量类型、模块/全局扩充、this is T类型谓词等至今仍被广泛使用的类型能力,同时在工具链层面打通了.js直接编译、AMD/System 单文件合并、自定义 JSX 工厂、tsconfig.json注释与--project灵活性。其中多数编译选项(--allowJs、--allowSyntheticDefaultImports、--noImplicitReturns、--noFallthroughCasesInSwitch、--pretty、--reactNamespace等)的默认值与约束都可以在编译选项清单中查证,相关的行为变更则记录在1.8 破坏性改动中,可作为升级或排查问题的第一手依据。
- 文档
- 教程
【免费下载链接】TypeScript
TypeScript 使用手册(中文版)翻译。http://www.typescriptlang.org
相关推荐
TypeSpec 字面量类型完全指南:字符串、多行字符串、模板字面量与数值/布尔字面量
TypeSpec 字面量类型完全指南:字符串、多行字符串、模板字面量与数值/布尔字面量 在设计 API 时,我们经常需要把接口结构定义成具体的字面量值——例如某
编程语言编译器后端TypeScript 4.1 新特性全解析:模版字面量类型、键重映射与破坏性改动
TypeScript 4.1 新特性全解析:模版字面量类型、键重映射与破坏性改动 TypeScript 4.1 是一次聚焦类型系统深水区的版本更新:它引入了 模
文档教程TypeScript 2.6 新特性详解:严格函数类型检查、模板字符串缓存与诊断体验升级
TypeScript 2.6 新特性详解:严格函数类型检查、模板字符串缓存与诊断体验升级 TypeScript 2.6 是语言在类型安全性与开发体验上的一次重要
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考