对编辑器的支持
TypeScript 最大的优势之一便是增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。
主流的编辑器都支持 TypeScript,这里我推荐使用 Visual Studio Code。
它是一款开源,跨终端的轻量级编辑器,内置了对 TypeScript 的支持。另外它本身也是用 TypeScript 编写的
全局模块
在默认情况下,当你开始在一个新的 TypeScript 文件中写下代码时,它处于全局命名空间中。如在 foo.ts 里的以下代码。
const foo = 123;
如果你在相同的项目里创建了一个新的文件 bar.ts,TypeScript 类型系统将会允许你使用变量 foo,就好像它在全局可用一样:
const bar = foo; // allowed
毋庸置疑,使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。我们推荐使用下文中将要提到的文件模块。
文件模块
文件模块也被称为外部模块。如果在你的 TypeScript 文件的根级别位置含有 import 或者 export,那么它会在这个文件中创建一个本地的作用域。因此,我们需要把上文 foo.ts 改成如下方式(注意 export 用法):
export const foo = 123;
在全局命名空间里,我们不再有 foo,这可以通过创建一个新文件 bar.ts 来证明:
const bar = foo; // ERROR: "cannot find name 'foo'"
如果你想在 bar.ts 里使用来自 foo.ts 的内容,你必须显式地导入它,更新后的 bar.ts 如下所示。
import { foo } from './foo';
const bar = foo; // allow
在 bar.ts 文件里使用 import 时,它不仅允许你使用从其他文件导入的内容,还会将此文件 bar.ts 标记为一个模块,文件内定义的声明也不会“污染”全局命名空间
文件模块详情与 module 选项
文件模块拥有强大的功能和较强的可用性。
你可以根据不同的 module 选项来把 TypeScript 编译成不同的 JavaScript 模块类型,这有一些你可以忽略的东西:
- AMD:不要使用它,它仅能在浏览器工作;
- SystemJS:这是一个好的实验,但已经被 ES 模块替代;
- ES 模块:它并没有准备好。
使用 module: commonjs 选项来替代这些模式,将会是一个好的主意。
那么 module 指定了我们的 Typescript 代码在最终编译时应该被编译成什么,但是在书写我们的 TypeScript 代码时,我们应该遵循什么规范呢?
- 放弃使用 import/require 语法即 import foo = require('foo') 写法
- 推荐使用 ES 模块语法
Note
:如果你使用了 module: commonjs 选项, moduleResolution: node 将会默认开启。该选项指定了当导入模块的路径不是相对路径时,模块解析将会模仿 Node 模块解析策略
- 当你使用 import * as foo from 'foo',将会按如下顺序查找模块:
./node_modules/foo
../node_modules/foo
../../node_modules/foo
直到系统的根目录
- 当你使用 import * as foo from 'something/foo',将会按照如下顺序查找内容
./node_modules/something/foo
../node_modules/something/foo
../../node_modules/something/foo
直到系统的根目录
编译出错时也会生成编译结果
尝试编译如下代码:
function sayHello(person: string) {
return 'Hello, ' + person;
}
let user = [0, 1, 2];
console.log(sayHello(user));
编辑器中会提示错误,编译的时候也会出错:
hello.ts:6:22 - error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
但是还是生成了 js 文件:
function sayHello(person) {
return 'Hello, ' + person;
}
var user = [0, 1, 2];
console.log(sayHello(user));
这是因为 TypeScript 编译的时候即使报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。
如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可。关于 tsconfig.json,请参阅官方手册(中文版)。
type 类似于类型中的变量
如下:使用 type 创建了一些联合类型,或者是某些类型的别名,和变量的概念很类似,只不过这里变量所对应的值不是普通的 JavaScript 值,而是 ts 中的类型
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
Note
:type 常用于创建联合类型,在 type 和 interface 都能实现的需求时,尽量用 interface,只有在 interface 无法实现的需求上才去使用 type
关于 namespace
namespace 是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间。
由于历史遗留原因,在早期还没有 ES6 的时候,ts 提供了一种模块化方案,使用 module 关键字表示内部模块。但由于后来 ES6 也使用了 module 关键字,ts 为了兼容 ES6,使用 namespace 替代了自己的 module,更名为命名空间。
随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的 namespace,而推荐使用 ES6 的模块化方案了,故我们不再需要学习 namespace 的使用了。
namespace 被淘汰了,但是在声明文件中,declare namespace 还是比较常用的,它用来表示全局变量是一个对象,包含很多子属性。
比如 jQuery 是一个全局变量,它是一个对象,提供了一个 jQuery.ajax 方法可以调用,那么我们就应该使用 declare namespace jQuery 来声明这个拥有多个子属性的全局变量。
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
// src/index.ts
jQuery.ajax('/api/get_something');
注意,在 declare namespace 内部,我们直接使用 function ajax 来声明函数,而不是使用 declare function ajax。类似的,也可以使用 const, class, enum 等语句:
// src/jQuery.d.ts
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
const version: number;
class Event {
blur(eventType: EventType): void
}
enum EventType {
CustomClick
}
}
// src/index.ts
jQuery.ajax('/api/get_something');
console.log(jQuery.version);
const e = new jQuery.Event();
e.blur(jQuery.EventType.CustomClick);
interface 和 type 的特殊性
全局变量的声明文件主要有以下几种语法:
declare var
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface 或 type
声明全局类型
可以看到,直接使用 interface 或 type 申明的类型的都是全局的接口或类型,这一点需要注意,也就是说,在 ts 项目中的任意一个 ts 文件中,如果直接使用了 interface 或 type,它们申明的类型都是全局变量的概念
import 或 export 关键词会导致模块化
在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了
第三方模块没有可用的申明文件时
一般使用第三方不是 TypeScript 编写的模块时,我们可以直接下载对应的声明文件:yarn add @types/{模块名}。
然而有些模块是没有对应的声明文件的,这时候就需要我们自己编写声明文件,以 rc-form 为例,只需在 src/@types/definition.d.ts 中(关于申明文件的位置,经过测试,Typescript 4.1.2版本,无需任何配置,放在项目中的非(dist、node_modules、src)的文件夹中都可以
)添加对应代码即可:
// definition.d.ts
declare module "rc-form" {
// 在此只是简单地进行类型描述
export const createForm: any;
export const createFormField: any;
export const formShape: any;
}
TS 中导入 .png、.json 等
不止是在 TypeScript 中导入未声明 JavaScript,导入.png、.json等文件时也同样需要去编写声明文件。
提供一种方式,可以创建一个声明文件src/@types/definition.d.ts(你也可以命名为其他),在其中编写如下声明:
// definition.d.ts
declare module '*.png' {
const value: any
export = value
}
declare module '*.vue' {
const value: any
export = value
}
// index.ts
// 之后在 TS 中导入也不会有问题
import avatar from './img/avatar.png'
import x from './x.vue'
隐式 any 类型(implicitly has an ‘any’ type)
当 tsconfig.json 中,"noImplicitAny": false 时,可以直接在 TypeScript 中引用 JavaScript(无声明文件)的库,所有的引入都会被默认为any类型。
但为了规范编码,总是默认 "noImplicitAny": true,这样当发生上述情况时,编译器会阻止编译,提示我们去给这些非 ts 文件 加上类型声明。
typescript 配置文件
{
"compilerOptions": {
/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似).
/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候,生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块,并在每个文件里加入 'use strict'
/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时,抛出错误
"noUnusedParameters": true, // 有未使用的参数时,抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时,抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)
/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表,其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入。
/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性
/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}
对编译的误解
起初我以为,TypeScript 编译是以 src 文件夹下的 index.ts 作为入口,通过模块系统,逐个编译需要用到的 ts 模块,类似于 webpack 打包的原理一样,那么项目中没有用到的 ts 模块就不会被编译
但实际情况并不是这样,Ts 编译时根本没有所谓的入口文件概念,而是将项目中所有的 ts 文件都按照之前的目录结构统一编译并输出至指定的目录(outDir选项)
编译时指定文件
默认情况下,TypeScript 编译时会将项目中的所有 ts 文件编译到指定输出目录
Note
:编译时不是只会编译 src 目录,是项目目录下的所有 ts 文件
那么除了这种默认的处理,你还可以使用 include 和 exclude 选项来指定需要包含的文件和排除的文件:
{
"exclude": [
"./folder/**/*.spec.ts",
"./folder/someSubFolder"
]
}
三斜线指令
与 namespace 类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。
但是在声明文件中,它还是有一定的用武之地。
类似于声明文件中的 import,它可以用来导入另一个声明文件。与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:
- 当我们在书写一个全局变量的声明文件时
- 当我们需要依赖一个全局变量的声明文件时
书写一个全局变量的声明文件
这些场景听上去很拗口,但实际上很好理解——在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了:
// types/jquery-plugin/index.d.ts
/// <reference types="jquery" />
declare function foo(options: JQuery.AjaxSettings): string;
// src/index.ts
foo({});
三斜线指令的语法如上,/// 后面使用 xml 的格式添加了对 jquery 类型的依赖,这样就可以在声明文件中使用 JQuery.AjaxSettings 类型了。
注意,三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释。
依赖一个全局变量的声明文件
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了:
// types/node-plugin/index.d.ts
/// <reference types="node" />
export function foo(p: NodeJS.Process): string;
// src/index.ts
import { foo } from 'node-plugin';
foo(global.process);
在上面的例子中,我们通过三斜线指引入了 node 的类型,然后在声明文件中使用了 NodeJS.Process 这个类型。最后在使用到 foo 的时候,传入了 node 中的全局变量 process。
由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。
以上两种使用场景下,都是由于需要书写或需要依赖全局变量的声明文件,所以必须使用三斜线指令。在其他的一些不是必要使用三斜线指令的情况下,就都需要使用 import 来导入。
拆分声明文件
当我们的全局变量的声明文件太大时,可以通过拆分为多个文件,然后在一个入口文件中将它们一一引入,来提高代码的可维护性。比如 jQuery 的声明文件就是这样的:
// node_modules/@types/jquery/index.d.ts
/// <reference types="sizzle" />
/// <reference path="JQueryStatic.d.ts" />
/// <reference path="JQuery.d.ts" />
/// <reference path="misc.d.ts" />
/// <reference path="legacy.d.ts" />
export = jQuery;
其中用到了 types 和 path 两种不同的指令。它们的区别是:types 用于声明对另一个库的依赖,而 path 用于声明对另一个文件的依赖。
上例中,sizzle 是与 jquery 平行的另一个库,所以需要使用 types="sizzle" 来声明对它的依赖。而其他的三斜线指令就是将 jquery 的声明拆分到不同的文件中了,然后在这个入口文件中使用 path="foo" 将它们一一引入。
自动生成声明文件
如果库的源码本身就是由 ts 写的,那么在使用 tsc 脚本将 ts 编译为 js 的时候,添加 declaration 选项,就可以同时也生成 .d.ts 声明文件了。
我们可以在命令行中添加 --declaration(简写 -d),或者在 tsconfig.json 中添加 declaration 选项。这里以 tsconfig.json 为例:
{
"compilerOptions": {
"module": "commonjs",
"outDir": "lib",
"declaration": true,
}
}
自动生成的声明文件基本保持了源码的结构,而将具体实现去掉了,生成了对应的类型声明。
使用 tsc 自动生成声明文件时,每个 ts 文件都会对应一个 .d.ts 声明文件。这样的好处是,使用方不仅可以在使用 import foo from 'foo' 导入默认的模块时获得类型提示,还可以在使用 import bar from 'foo/lib/bar' 导入一个子模块时,也获得对应的类型提示。
发布声明文件
当我们为一个库写好了声明文件之后,下一步就是将它发布出去了。
此时有两种方案:
- 将声明文件和源码放在一起
- 将声明文件发布到 @types 下
这两种方案中优先选择第一种方案。保持声明文件与源码在一起,使用时就不需要额外增加单独的声明文件库的依赖了,而且也能保证声明文件的版本与源码的版本保持一致。
仅当我们在给别人的仓库添加类型声明文件,但原作者不愿意合并 pull request 时,才需要使用第二种方案,将声明文件发布到 @types 下。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!