管理TypeScript项目中的类型声明
说来惭愧,最近才正儿八经地在生产项目中使用TypeScript
,遇见一个比较棘手的问题就是:如何管理项目中定义的各种类型声明。
本文将从TS项目和声明方式开始,探究如何解决该问题。
参考
ts项目
tsconfig.json 编译上下文
如果一个目录下存在一个tsconfig.json
文件,那么它意味着这个目录是TypeScript项目的根目录。
TypeScript 将 会把此目录和子目录下的所有 .ts 文件作为编译上下文的一部分。
tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。
不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录。
不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
当命令行上指定了输入文件时,tsconfig.json
文件会被忽略。
全局模式和模块模式
在一个TypeScript项目中,当一个x.ts
文件不包含import
或export
关键字时,它就是一个全局模式;反之就是一个文件模块
- 全局模式,所有变量定义,类型声明都是全局的,同名
interface
会进行合并,同名变量或type会报错 - 文件模块,所有变量定义,类型声明都是在模块内有效的,可以通过import引入文件模块中export出来的变量和类型
一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。
如果声明文件是全局变量的模式,其他*.ts
文件就可以获取到其中的类型了。
在文件模块中,也可以通过declare global
的方式进入到全局命名空间。
// 当前文件为文件模块
export {}
// 进入全局命名空间
declare global {
// global namespace
// 里面的声明的内容是全局作用于
}
第三方js库的类型:声明文件
TypeScript是JavaScript的超集,在ts项目中可能需要依赖js的第三方库。比如现在有个库Mod1.js
,暴露了一些接口
window.Mod1 = {
// 参数为字符串
test(msg){
console.log(msg)
}
}
在写代码的时候,如果不做任何操作,无法使用ts的类型检测等功能
// 1.ts
Mod1.test(123) // 无法检测到错误
这时候就需要一个描述 JavaScript 库和模块信息的声明文件,借助这个文件,就可以利用TypeScript 的各种特性来使用库文件了。
声明文件以d.ts
结尾,注意它只有类型声明,不包含代码实现(类似于C语言中的.h
文件)
declare namespace Mod1 {
function test(msg:string):void;
}
然后引入该声明文件
/// <reference path = "mod1.d.ts" />
Mod1.test(123)
这个时候再写代码或者tsc 1.ts
编译,就会看见错误提示了
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
需要注意的是上面我们在运行tsc
时指定了输入文件,会忽略tsconfig.json
因此需要手动引入声明文件;如果是TypeScript项目模式下,全局模块可以自动引入,而无需再手动引入。@types
@types
第三方模块那么多,每个库都写一遍声明文件,听起来就很麻烦,DefinitelyTyped提供了大量第三方库的声明。
比如jQuery,只需要安装@types/jquery
即可
npm install @types/jquery
因此在准备编写第三方库的声明文件之前,可以先去瞅一瞅社区是否已经有对应的实现了。
三斜线指令
可以看见上面通过三斜线///
引入了声明文件,这是一种特殊的指令,也是早期ts的模块化标签。
/// <reference types="sizzle" />
/// <reference path="JQuery.d.ts" />
这里展示了types
和path
两种语法
types
用于声明对另一个库的依赖path
用于声明对另一个文件的依赖,在同一个库中如果拆分了声明文件,就可能需要使用
其他还有一些废弃的语法,这里就不做介绍了。
由于全局声明文件不允许出现import、export等关键字(出现了就不是全局声明文件了),因此当
- 当我们在书写一个全局变量的声明文件时,不允许出现export
- 当我们需要依赖一个外部项目的全局变量声明文件时,不允许出现import
这些场景的时候,三斜线还是有用武之地的;除此之外,基本上不再建议使用三斜线语法了。
命名空间
全局声明最容易出现的问题就是命名冲突,TS早期时为了解决模块化而创造的关键字module,后来ES6使用了module关键字,因此TS使用namespace
代替了module,中文名为命名空间。
命名空间主要是为了解决全局命名冲突,随着es6模块的普及,现在已经不再推荐使用命名空间了。
但在全局声明文件中,declare namespace
还是比较常用的,主要用来表示全局变量是一个对象包含很多子属性和方法的情况。
namespace Mod2 {
export function test1(msg) {
console.log(msg);
}
function test2(msg) {
console.log(msg);
}
}
可以看到编译后的文件,就是将命名空间内的方法挂载到全局对象Mod2上
var Mod2;
(function (Mod2) {
function test1(msg) {
console.log(msg);
}
Mod2.test1 = test1;
// test2没有被export,则没有挂载到命名空间对象上
function test2(msg) {
console.log(msg);
}
})(Mod2 || (Mod2 = {}));
模块
模块声明就跟常规的es6模块类似,可以直接在ts文件中通过export导出类型,就像是导出一个模块方法
export type1 = {
x: number
}
export function test(){
console.log('test')
}
类型跟模块方法的使用基本一致,在需要到模块类型的地方,直接在文件头部import即可。
但是这种类型于实现混在一起的写法,可能会导致整个项目看起来比较混乱。一种常见的做法是将类型定义单独放在文件找那个,
// types.ts
export type1 = {
x: number
}
export interface iProps = {
y: string
}
// ... 其他的类型
一个文件就是一个模块,当然也可以按照业务和功能拆分多个types文件。
扩展模块类型
如果是需要扩展原有模块的话,需要在类型声明文件中先引用原有模块
比如下面声明,在moment上扩展某个方法
import * as moment from 'moment';
declare module 'moment' {
export function test(): moment.CalendarKey;
}
shims-vue.d.ts
参考:
ts 默认只识别 .d.ts、.ts、.tsx 后缀的文件,考虑到部分JS模块加载工具支持引入非JavaScript文件,因此提供了通过前后缀来处理这种加载
在Vue TypeScript项目中,就需要shims-vue.d.ts
为所有vue文件做模块声明
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
全局声明
目前看来,大部分场景下都应该使用文件模块来管理我们的代码和类型。但在某些时候,全局声明也是有一些用处的。
global.d.ts
对于一个从JavaScript迁移的项目、或者团队中包含TypeScript新手的时候,可以提供一个全局声明文件。
按照习惯一般叫做global.d.ts
(当然也可以是foo.d.ts
之类的),如Vue3中的全局类型文件
利用全局声明(不包含export、import等关键字),在global.d.ts
中编写的类型和接口会放入全局命名空间里,这些类型可以在当前TS项目所有的TypeScript文件中直接使用。
lib.d.ts
事实上TypeScript内置了一个全局的声明文件lib.d.ts
,里面主要是一些JavaScript运行时及DOM各种变量及环境声明(如:window
、document
、math
)和一些类似的接口声明(如:Window
、Document
、Math
)。
可以通过配置noLib
选项来取消自动添加lib.d.ts
(当然一般不建议这样做)。
这里介绍lib.d.ts
主要是为了强调global.d.ts
,在某些情况下,如果需要扩展运行时的某些字段,(如向Date原型添加一个方法、声明一个全局变量等),可以在全局声明文件中进行扩展
// interface类型会进行合并
interface Date {
toMoment(): void;
}
为了便于维护,这些操作一般也会在global.d.ts
中进行。
全局声明的语法
在全局声明文件中,主要有下面声明全局变量、类型的方法
declare var // 声明全局变量
declare function // 声明全局方法
declare class // 声明全局类
declare enum // 声明全局枚举类型
declare namespace //声明全局对象(含有子属性)
interface // 声明全局接口
type //声明全局类型
如何管理项目中的类型
一般来讲,你组织声明文件的方式取决于库是如何被使用的。
使用全局声明的场景
全局声明会向整个ts项目添加全局的类型、接口等,因此需要考虑命名冲突等问题。
除了全局模块之外和整个项目通用的类型之外,应尽可能避免使用全局声明。
如果需要声明全局类型,也应该统一放在global.d.ts
中进行管理,而不能在模块文件中通过declare namespace global
随意注册全局类型。
使用文件模块声明的场景
上面看到了declare module
、declare namepsace
、三斜线指令、import/export
等各种模块声明方式,整理下来不禁感慨,ts也残留了很多历史问题,整个模块管理比较混乱。
感觉还是统一使用ES6模块最稳妥,类型来源、依赖都一清二楚,也不用考虑命名冲突等问题。
至于频繁import多些几行代码的问题,在项目可维护性面前,应该是无足轻重的。
在大型项目中,推荐使用声明文件(Declaration Files
)来管理接口或其他自定义类型,如后台服务响应model、API参数等类型
内部类型
通用的类型声明应该单独放在声明文件中进行管理,对于那些不通用的类型,如组件props等,应该就近声明。
interface还是type
interface
只能声明对象类型,支持声明合并(可扩展)
interface User {
id: number
}
interface User {
name: string
}
// 包含id和name两个属性
const a: User = {
id: 1,
name: "shymean"
}
type
不支持声明合并,更像是let
或const
声明了一个”类型变量“,主要用于定义类型别名,可以是任意类型、类型推算等,更为通用
type A = {
id: 1
}
type A = {
name: string
}
// 提示 Duplicate identifier 'A'.ts(2300)
如果是在开发模块,允许别人进行类型扩展,就是用interface
;如果需要定义基础类型或类型运行,就使用type
小结
上面整理了TypeScript中关于类型声明的一些知识点,然后总结了管理TS项目中的类型。
上述的管理方式都是翻阅文档和个人体感得到的一些建议,打算在项目中使用一段时间后再回头补充。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。