TypeScript 介绍
- TypeScript 是 JavaScript 的超集,提供了 JavaScript 的所有功能,并提供了可选的静态类型、Mixin、类、接口和泛型等特性。
- TypeScript 的目标是通过其类型系统帮助及早发现错误并提高 JavaScript 开发效率。
- 通过 TypeScript 编译器或 Babel 转码器转译为 JavaScript 代码,可运行在任何浏览器,任何操作系统。
- 任何现有的 JavaScript 程序都可以运行在 TypeScript 环境中,并只对其中的 TypeScript 代码进行编译。
- 在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型定义来提高代码的可维护性,减少可能出现的 bug。
- 永远不会改变 JavaScript 代码的运行时行为,例如数字除以零等于 Infinity。这意味着,如果将代码从 JavaScript 迁移到 TypeScript ,即使 TypeScript 认为代码有类型错误,也可以保证以相同的方式运行。
- 对 JavaScript 类型进行了扩展,增加了例如
any
、unknown
、never
、void
。 - 一旦 TypeScript 的编译器完成了检查代码的工作,它就会 擦除 类型以生成最终的“已编译”代码。这意味着一旦代码被编译,生成的普通 JS 代码便没有类型信息。这也意味着 TypeScript 绝不会根据它推断的类型更改程序的 行为。最重要的是,尽管可能会在编译过程中看到类型错误,但类型系统自身与程序如何运行无关。
- 在较大型的项目中,可以在单独的文件 tsconfig.json 中声明 TypeScript 编译器的配置,并细化地调整其工作方式、严格程度、以及将编译后的文件存储在何处。
编译选项
TypeScript 提供了很多不同功能的编译选项,既可以通过配置 tsconfig.json 文件中的 compilerOptions
属性来实现编译,也可以使用在 tsc
命令后跟随参数这种形式,直接编译 .ts
文件。
以下这些选项可以同时在命令行和 tsconfig.json 里使用。
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
–-allowJs | boolean | false | 允许编译 JavaScript 文件 |
–-allowSyntheticDefaultImports | boolean | false | 允许从没有设置默认导出的模块中默认导入 |
–-allowUnreachableCode | boolean | false | 不报告执行不到的代码错误 |
–-allowUnusedLabels | boolean | false | 不报告未使用的标签错误 |
–-alwaysStrict | boolean | false | 以严格模式解析并为每个源文件生成 “use strict” 语句 |
-–checkJs | boolean | false | 在 .js 文件中报告错误,与 –allowJs 配合使用 |
-–declaration -d | boolean | false | 生成相应的 .d.ts 文件 |
-–declarationDir | string | 生成声明文件的输出路径 | |
-–diagnostics | boolean | false | 显示诊断信息 |
–-experimentalDecorators | boolean | false | 启用实验性的ES装饰器 |
–-extendedDiagnostics | boolean | false | 显示详细的诊断信息 |
–-forceConsistentCasingInFileNames | boolean | false | 禁止对同一个文件的不一致的引用 |
–-inlineSourceMap | boolean | false | 生成单个 sourcemaps 文件,而不是将每 sourcemaps 生成不同的文件 |
–-inlineSources | boolean | false | 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 –inlineSourceMap 或 –sourceMap 属性 |
–init | 初始化 TypeScript 项目并创建一个 tsconfig.json 文件 | ||
–-listEmittedFiles | boolean | false | 打印出编译后生成文件的名字 |
–-listFiles | boolean | false | 编译过程中打印文件名 |
–module -m | string | target == “ES6” ? “ES6” : “commonjs” | 指定生成哪个模块系统代码: “None”、”CommonJS”、”AMD”、”System”、”UMD”、”ES6” 或 “ES2015”。 ► 只有 “AMD” 和 “System” 能和 –outFile 一起使用。 ► “ES6” 和 “ES2015” 可使用在目标输出为 “ES5” 或更低的情况下。 |
–moduleResolution | string | module == “AMD” or “System” or “ES6” ? “Classic” : “Node” | 决定如何处理模块 |
–noEmit | boolean | false | 不生成输出文件 |
–noEmitHelpers | boolean | false | 不在输出文件中生成用户自定义的帮助函数代码,如 __extends 。 |
–noEmitOnError | boolean | false | 报错时不生成输出文件 |
–noErrorTruncation | boolean | false | 不截短错误消息 |
–noFallthroughCasesInSwitch | boolean | false | 报告 switch 语句的 fallthrough 错误(即不允许 switch 的 case 语句贯穿) |
–noImplicitAny | boolean | false | 在表达式和声明上有隐含的 any 类型时报错。 |
–noImplicitReturns | boolean | false | 当不是函数的所有返回路径都有返回值时报错 |
–noImplicitThis | boolean | false | 当 this 表达式的值为 any 类型时生成一个错误 |
–noImplicitUseStrict | boolean | false | 模块输出中不包含 “use strict” 指令 |
–noLib | boolean | false | 不包含默认的库文件( lib.d.ts) |
–noResolve | boolean | false | 不把 /// <reference> 或模块导入的文件加到编译文件列表 |
–noStrictGenericChecks | boolean | false | 禁用在函数类型里对泛型签名进行严格检查 |
–noUnusedLocals | boolean | false | 若有未使用的局部变量则抛错 |
–noUnusedParameters | boolean | false | 若有未使用的参数则抛错 |
–outDir | string | 重定向输出目录 | |
–-outFile | string | 将输出文件合并为一个文件,合并的顺序是根据传入编译器的文件顺序和 ///<reference> 和 import 的文件顺序决定的。 |
|
–preserveConstEnums | boolean | false | 保留 const enum 声明 |
–preserveSymlinks | boolean | false | 不把符号链接解析为其真实路径;将符号链接文件视为真正的文件 |
–preserveWatchOutput | boolean | false | 保留 watch 模式下过时的控制台输出 |
–project -p | string | 编译指定目录下的项目,这个目录应该包含一个 tsconfig.json文件来管理编译 | |
–removeComments | boolean | false | 删除所有注释,除了以 /!* 开头的版权信息 |
–-skipDefaultLibCheck | boolean | false | 忽略库的默认声明文件的类型检查 |
–-skipLibCheck | boolean | false | 忽略所有的声明文件( *.d.ts )的类型检查 |
–sourceMap | boolean | false | 生成相应的 .map 文件 |
–sourceRoot | string | 指定 TypeScript 源文件的路径,以便调试器定位。当 TypeScript 文件的位置是在运行时指定时使用此标记,路径信息会被加到 sourceMap 里。 | |
–strict | boolean | false | 启用所有严格类型检查选项,相当于启用 –noImplicitAny、–noImplicitThis、–alwaysStrict、–strictNullChecks、–strictFunctionTypes 和 –strictPropertyInitialization。 |
–strictFunctionTypes | boolean | false | 禁用函数参数双向协变检查 |
–strictPropertyInitialization | boolean | false | 确保类的非 undefined 属性已经在构造函数里初始化,需要同时启用 –strictNullChecks。 |
–strictNullChecks | boolean | false | 在严格的 null 检查模式下,null 和 undefined 值不包含在任何类型里,只允许用它们自己和 any 来赋值(有个例外,undefined 可以赋值到 void)。 |
–target -t | string | ES3 | 指定 ECMAScript 目标版本 ES3(默认)、ES5、ES6/ ES2015、ES2016、ES2017 或 ESNext。 注意:ESNext 最新的生成目标列表为 ES proposed features。 |
–traceResolution | boolean | false | 生成模块解析日志信息 |
–types | string[] | 要包含的类型声明文件名列表 | |
–typeRoots | string[] | 要包含的类型声明文件路径列表 | |
–-watch -w | 在监视模式下运行编译器,会监视输出文件,在它们改变时重新编译。监视文件和目录的具体实现可以通过环境变量进行配置。 |
tsconfig.json
- 可以通过
tsc --init
命令在根目录生成tsconfig.json
文件。 - 目录中存在
tsconfig.json
文件表示该目录是 TypeScript 项目的根目录。 tsconfig.json
文件指定编译项目所需的根文件和编译器选项,主要有以下配置项:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
"compilerOptions": {},
"files": [
"core.ts",
"index.ts",
"types.ts"
],
"exclude": [
"node_modules",
"lib",
"**/*.test.ts"
],
"include": [
"src/**/*"
],
"extends": "@tsconfig/recommended/tsconfig.json"
}
compilerOptions
- 对象类型,用来设置编译选项,若不设置则默认使用上述编译选项的默认配置。files
- 指定一个包含相对或绝对文件路径的列表,不支持 glob 匹配模式。include
- 指定一个文件 glob 匹配模式列表。exclude
- 排除一个文件 glob 匹配模式列表。extends
- 字符串类型,指向另一个要继承的配置文件的路径。例如,可以继承一个推荐配置npm i @tsconfig/recommended
,"extends": "@tsconfig/recommended/tsconfig.json"
。- 如果有同名配置,继承文件里的配置会覆盖源文件里的配置。
- 配置文件里的相对路径在解析时相对于它所在的文件。
glob 通配符有:
*
匹配 0 或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录
- 如果一个 glob 模式里的某部分不包含文件扩展名(只包含
*
或.*
),那么仅有支持的文件扩展名类型被包含在内(默认情况下为 .ts、.tsx 和 .d.ts),如果allowJs
设置为true
,也包括 .js 和 .jsx。 - 如果
files
和include
都没有被指定,编译器默认包含当前目录和子目录下所有的 TypeScript 文件(.ts、.tsx 和 .d.ts),排除在exclude
里指定的文件。 - 如果同时指定了
files
或include
,编译器会将它们结合一并包含进来。 - 使用
include
引入的文件可以使用exclude
属性过滤。然而,通过files
属性明确指定的文件却总是会被包含在内,不管exclude
如何设置。 - 使用
outDir
指定的目录下的文件永远会被编译器排除,除非明确地使用files
将其包含进来(这时就算用exclude
指定也没用)。 - 如果没有特殊指定,
exclude
默认情况下会排除 node_modules、bower_components、jspm_packages 和outDir
目录。 - 任何被
files
或include
指定的文件所引用的文件也会被包含进来。例如,A.ts
引用了B.ts
,因此B.ts
不能被排除,除非引用它的A.ts
在exclude
列表中。 - 编译器不会去引入那些可能作为输出的文件。例如,我们包含了
index.ts
,那么index.d.ts
和index.js
会被排除在外。 - 优先级:命令行配置 >
files
>exclude
>include
。
declaration
用来为工程中的每个 TypeScript 或 JavaScript 文件生成 .d.ts
文件,这些 .d.ts
文件是描述模块外部 API 的类型定义文件。编辑工具可以通过 .d.ts
文件为非类型化的代码提供 intellisense 和精确的类型。
当 declaration
设置为 true
时,用编译器执行下面的 TypeScript 代码:
1 | export let helloWorld = "hi"; |
将会生成如下这样的 index.js
文件:
1 | export let helloWorld = "hi"; |
以及一个相应的 helloWorld.d.ts
:
1 | export declare let helloWorld: string; |
当使用 .d.ts
文件处理 JavaScript 文件时,需要使用 emitDeclarationOnly
或 outDir
来确保 JavaScript 文件不会被覆盖。
strictFunctionTypes
- 协变:允许子类型转换为父类型(可以里式替换 LSP 原则进行理解)。
- 逆变:允许父类型转换为子类型。
- 在函数的参数类型中,是符合逆变的,函数的关系和参数的关系是相反的。
- 在老版本的 TS 中,函数参数是双向协变的。也就是说,既可以协变又可以逆变,但是这并不是类型安全的。
- 在新版本 TS(2.6+)中 ,可以通过开启
strictFunctionTypes
来修复这个问题。设置之后,函数参数就不再是双向协变的了,函数参数检查更正确。
下面是一个禁用 strictFunctionTypes
的示例:
1 | // @strictFunctionTypes: false |
启用 strictFunctionTypes
后,将正确检测到错误:
1 | // @strictFunctionTypes: true |
在此功能的开发过程中,发现了大量本质上不安全的类层次结构,包括 DOM 中的一些。因此,该设置仅适用于以函数语法编写的函数,不适用于方法语法中的函数:
1 | // @strictFunctionTypes: true |
typeAcquisition
对象类型,用以设置自动引入库类型定义文件(.d.ts),该属性下面有3个子属性:
enable
: 布尔类型,用以设置是否开启自动引入库类型定义文件include
: 数组类型,允许自动引入的库名列表,如["jquery", "kendo-ui"]
exclude
: 数组类型,排除的库名列表
代码提示的秘密 - d.ts
- 在使用 TypeScript 的时候,最大的一个好处就是可以给 JS 各种类型约束,使得 JS 能够完成静态代码分析,推断代码中存在的类型错误或者进行类型提示。
- 而 TypeScript 完成类型推断,需要事先知道变量的类型,如果我们都是用 TypeScript 书写代码,并且给变量都指定了明确的类型,TypeScript 是可以很好的完成类型推断工作的。
- 但是有时,我们不免会引入外部的 JS 库,这时 TypeScript 就对引入的 JS 文件里变量的具体类型不明确了,为了告诉 TypeScript 变量的类型,因此就有了类型定义文件
d.ts
(d 即declare
),TypeScript 的声明文件。 - 如何让这些第三方库也可以进行类型推导呢?需要考虑如何让 JS 库也能定义静态类型。JavaScript 和 TypeScript 的静态类型交叉口 — 类型定义文件,类似于 C/C++ 的
.h
头文件(#include <stdio.h>
),轻松让 JavaScript 也能支持定义静态类型。 d.ts
文件用于为 TypeScript 提供有关用 JavaScript 编写的 API 的类型信息。简单讲,就是你可以在 ts 文件中调用的 js 文件的声明文件。- TypeScript 的核心在于静态类型,我们在编写 TS 的时候会定义很多的类型,但是主流的库都是 JS 编写的,并不支持类型系统。这个时候你不能用 TS 重写主流的库,我们只需要编写仅包含类型注释的
d.ts
文件,然后在你的 TS 代码中,可以在仍然使用纯 JS 库的同时,获得静态类型检查的优势。 - 在此期间,解决的方式经过了许多的变化,从 DefinitelyTyped 到 typings(已停止维护)。最后是 @types。在 Typescript 2.0 之后,推荐使用 @types 方式,TypeScript 将会默认地检查
./node_modules/@types
文件夹,自动从这里来获取模块的类型定义,当然了,你需要独立安装这个类型定义。Microsoft 在 The Future of Declaration Files 介绍了 TypeScript 的这个新特性。
类型路径 - @types
默认情况下,所有的
@types
包都会在编译时应用,任意层的node_modules/@types
都会被使用,进一步说,在node_modules/@types
中的任何包都被认为是可见的,这意味着包含了./node_modules/@types/
、../node_modules/@types/
、../../node_modules/@types/
中所有的包。如果你的类型定义不在上面这个默认文件夹中,可以使用
typesRoot
来配置,只有typeRoots
下面的包才会被包含进来。例如:1
2
3
4
5{
"compilerOptions": {
"typeRoots": ["./typings", "./vendor/types"]
}
}这个配置文件将包含
./typings
和./vendor/types
下的所有包,而不包括./node_modules/@types
下的。其中所有的路径都是相对于tsconfig.json
。当
types
被指定,则只有列出的包才会被包含在全局范围内。例如:1
2
3
4
5{
"compilerOptions": {
"types": ["node", "jest", "express"]
}
}这个配置文件将只会包含
./node_modules/@types/node
、./node_modules/@types/jest
和./node_modules/@types/express
。其他在node_modules/@types/*
下的包将不会被包含。此功能与typeRoots
不同的是,它只指定你想要包含的具体类型,而typeRoots
支持你想要特定的文件夹。可以指定
"types": []
来禁用自动引入@types
包。自动引入只在你使用了全局的声明(相反于模块)时是重要的,如果你使用import "foo"
语句,TypeScript 仍然会查找node_modules
和node_modules/@types
文件夹来获取foo
包。types
选项不会影响@types/*
如何被包含在你的代码中,例如:1
2import * as moment from "moment";
moment().format("MMMM Do YYYY, h:mm:ss a");moment
导入会有完整的类型。当你设置了不在types
数组中包含它们时,它将:
- 不会在你的项目中添加全局声明(例如 node 中的
process
或 Jest 中的expect
)。 - 导出不会出现在自动导入的建议中。
d.ts 和 @types 的关系
@types
是 npm 的一个分支,用来存放 d.ts
文件,如果对应的 npm 包存放在 @types
中,要使用必须下载!如果是自己本地的 d.ts
申明文件,则和 @types
没有任何关系!
实验
以下 baby.ts
文件,导出了一个 Baby 类,和一个叫 baby 的实例。Baby
包含一个私有的字段 _name
,静态的方法 smile
,公开的方法 getBabyName
, 在通过 new
调用 constructor
的时候,会初始化我们的 _name
,而 getBabyName
就是拿到我们私有的 _name
,之所以需要 getBabyName
,是因为通过 private
关键字指定的私有字段和方法,在实例中是无法访问的。
1 | export class Baby { |
我们加上 -d
选项编译 ts 文件:
1 | tsc baby.ts -d |
会有一个编译后的 baby.js
文件,你还会发现我们多出了一个 baby.d.ts
文件。大多数 ts 初学者会这样问:请问一下,如何在 ts 文件里面,引入已经写好的 js 文件呢?答案就在这里,d.ts 文件。
1 | export declare class Baby { |
我们发现 baby.ts
里面所有的方法声明都被导入到了 baby.d.ts
文件里面,而 TypeScript 恰恰就是通过这个 d.ts
文件进行代码提示的。
- 现在重命名一下我们的
baby.ts
,把它改成baby.copy.ts
。 - 新建
main.ts
文件,当使用import { baby } from "./baby";
语句导入的时候,VSCode 会自动提示baby.d.ts
和baby.copy.ts
。 - 我们选择
baby.d.ts
(baby.js
模块文件的声明文件),然后再敲baby.
,此时我们就看到了getBabyName
方法的提示。 - 如果删除
baby.d.ts
文件,会发现提示警告:无法找到模块“./baby”的声明文件。“baby.js”隐式拥有 "any" 类型。
添加自己的 typings 文件夹
如何解决没有库的 d.ts 文件时报错?
- 添加
typeRoots
配置项,就可以加载自己的 d.ts 文件了。1
2
3
4
5{
"compilerOptions": {
"typeRoots": ["typings"]
}
} - 在 typings 目录下新建一个 xxx.d.ts ,xxx 可以随意写。
1
2
3
4
5
6
7
8
9declare module "koa" {
interface Context {
render(filename: string, ...args: any[]) : any;
session: any;
i18n: any;
csrf: any;
flash: any;
}
} "koa"
就是你的报错库的名称,这里就只是给koa
库添加一些属性,防止代码编辑器报错。- 还有一点要注意的是,报错一定是因为该包主目录下没有一个
index.js
,或者放到lib
目录下面了,新版本的 TypeScript 只要你安装了库,并且它的下面有index.js
就可以加载到,不会报错但是会让你导入的是any
类型。
如何发布 d.ts 文件
第一种方式就是在你的库下面的
package.json
里面配置。这里最好写上相对路径:1
2
3
4"types": "./lib/main.d.ts"
// or
"typings": "./lib/main.d.ts"如果你的项目没有使用模块系统的话,可以将包中包含类型定义的
.d.ts
文件手动通过/// <reference path="" />
引入。第二种方式是给这个地址提交 PR。
1
https://github.com/DefinitelyTyped/DefinitelyTyped.git
- 最近的构建都具有完善的 类型标注:
- 所有的包基于 typescript@next 版本都有完善的类型标注:
- 所有的包都会在1小时30分钟内 发布到 npm:
- typescript-bot 在 Definitely Typed 一直处于活跃状态
Definitely Typed
是一个高质量的 TypeScript 类型定义的仓库。
- npm 包中并不总是有可用的类型,可能有时项目不再维护,有时他们不感兴趣,或没有时间使用 TypeScript。
- 由于缺少类型,在 TypeScript 中使用非类型化 npm 包将不会再具有类型安全性。
- 为了帮助 TypeScript 开发人员使用这些包,有一个社区维护的项目叫做 Definitely Typed。
- Definitely Typed 是一个为没有类型的 NPM 包提供类型脚本定义的中央存储库的项目。
- 安装声明包后,通常不需要其他步骤来使用类型,TypeScript 会在使用包本身时自动选择类型。编译器中会自动引入这些类型。如果你的项目没有使用模块系统的话,你可能需要使用
1
2npm install --save-dev @types/jquery
npm install --save-dev @types/nodetypes
指令进行手动引用:1
/// <reference types="node" />
- 当缺少类型时,VSCode 等编辑器通常会建议安装此类包。对于 npm 包 “foo”,它的类型定义的包名应该是 “@types/foo”。如果没有找到你的包,请在 TypeSearch 查询。
- Definitely Typed 和 npm 上的 @types 包有什么关系?Definitely Typed GitHub 仓库 master 分支 会通过 DefinitelyTyped-tools 自动发布到 npm 上的 @types。