1背景

  • 背景:随着TypeScript项目越来越庞大,项目中会有很多类型声明,有时A开发一个模块时,声明了 TypeA,B声明 TypeB,这两个类型非常相似,但由于A不知道B也声明了类似的类型,导致两人重声明,可能每个人存放类型文件的位置也不一样,日积月累,项目中的类型文件越来越混乱。
  • 整体思路:参考现有方法,并针对项目已有情况进行类型文件整理,规范项目结构。

2常见方案

2.1 常见方式

方式1 全局唯一 types.ts 文件

  • 优点:方便存储,只需要维护一个文件,类型可以进行复用,方便查看相似类型,避免多次编写。
  • 缺点:如果项目很大,文件会上千行,也会导致命名困难,非常不利于维护、合并冲突将是经常遇到的问题。
1
2
3
|--src
|-- types.d.ts // 全局的类型文件

  • 优化方法1:可以根据一定维度(页面、组件、接口等)划分 namespace

声明时:

使用时:
直接namespace.Type 即可使用,无需导入。

  • 优化方法2:可以通过 /** */ 形式的注释为给 TypeScript 类型做标记提示:

声明时:

使用时:

方式2 按模块(页面、组件、接口等)划分

  • 优点:按模块划分,
  • 缺点:容易多次编写重复类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
|--src
|-- types
|-- common // 公共类型
|-- common9.ts
|-- components // 组件的类型
|-- button.ts
|-- api // 页面中的请求类型
|-- api1.ts
|-- pages // 页面中的props等类型
|-- page2.ts
|-- store // 全局 状态类型
|-- store4.ts
...

方式3 基于组件

基于组件的话,针对每个组件,可以有一个.types.ts,.model.ts文件

1
2
3
4
5
6
7
8
9
10
11
src
|--
|--app
|--components
|--car
|-- car.component.css
|-- car.component.types.ts
|-- car.component.html
|-- car.component.spec.ts
|-- car.component.ts
|-- car.module.ts

优点:每个类型有有自己的module,多个组件可以存在相同的类型定义
缺点:存在重复的接口,类型复用比较难

方式4 基于模型或基于对象

定义在整个代码库中使用的模型,无论是父组件还是子组件都可以使用相同的模型

1
2
3
4
5
6
src
|--
|--models
|-- car.model.ts
|-- user.model.ts
|-- brand.model.ts

优点:类型可以进行复用,避免多次编写
缺点:随着项目的迭代,如果不提出多个相似的名称,就不能拥有类型或接口的不同变体

方式5 基于类型

另一种可以使用的方法是基于类型的方法。其工作方式是将枚举、类型、接口、类存储在它们自己的文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
|--
|--ts
|--enums
| |-- a.enums.ts
| |-- b.enums.ts
|--interfaces
| |-- a.interfaces.ts
| |-- b.interfaces.ts
| |-- c.interfaces.ts
| |-- d.interfaces.ts
|--types
|-- a.types.ts
|-- b.types.ts

方式6 混合使用

混合方法使用全局和基于组件或模型的方法。这样,可以将类型和接口存储在项目中通用的全局文件夹中。此外,可以存储与特定组件、控制器或服务严格相关的特定类型和接口.
推荐混合使用上述组织TypeScript的interface和types的方法,针对可以复用的interface或者types可以使用唯一的types.ts文件或者基于模型或者基于对象或者基于类型的方式来组件自己的接口定义,针对不可以复用的定义,我们应该尽量保证特定的类型引用应该更接近自己的引用,可以使用基于组件的方案,或者直接将类型编写到文件中,减少维护类型的成本,避免类型导出

2.2 调研业内组织和存储类型文件的方法

  • TypeScript 团队 使用一个****types.ts ****统一管理所有类型:链接
  • Vite团队 使用 types****文件夹 按模块来管理类型:链接
  • image.png
  • Tencent团队 使用 从组件/页面等模块导出 类型来管理类型链接
  • Tencent :

image.png

2.3 项目现状

为了使用vue composion API,项目从vue2.6升级到vue2.7;
源项目是使用JavaScript,为了便于维护项目的一个稳定性等,引入使用typescript。

采用方案

初步使用:按照模块(页面、组件、接口等)划分,容易查找使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|--src
|-- shared
|-- enums
|-- auth.ts
|-- step.ts
|-- mappings
|--auth.ts
|-- types
|-- common.d.ts // 公共类型
|-- button.d.ts // 组件涉及数据的类型(把基本数据、请求接口传参数据定义,用于公用)
// |-- api // 页面中的请求类型 (通过直接在api请求里面使用数据ts定义)
// |-- pages // 页面中的props等类型(直接使用数据ts定义)
// |-- store // 全局 状态类型(定义在store文件夹本身)

...

types文件夹

types下的声明文件,不再更细粒度地划分文件夹去管理,而是直接定义一个数据块的命名空间,在各自数据块的命名空间,管理相关数据类型(便于复用),并且同时生成函数请求方法传参需要的数据类型。
例如resource.d.ts:
生命一个同名的命名空间,下面定义基本数据的interface,里面的interface可以使用彼此(复用),对于一些操作等函数,可以在里面声明一个命名空间,用于构造函数的传参等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
declare namespace Resource {
interface CanvasImageShape {
scale: number;
left: number;
top: number;
naturalWidth: number;
naturalHeight: number;
scaledWidth: number;
scaledHeight: number;
isFirstWidth: boolean;
}


namespace Create {
interface Data {
Name: string;
}

interface FormData {
Name: Resource.Instance["Name"];
}
}

namespace Upload {
interface Data {
File: File | null;
ID?: Resource.InstanceItems["ID"];
}
}

namespace Unzip {
interface Data {
ID?: Resource.InstanceItems["ID"];
}
}

namespace EnterLabel {
interface Data {
ID: Resource.InstanceItems["ID"];
}
}

namespace List {
interface Params {
ID?: string;
Name?: string;
page: number;
page_size: number;
}
}
namespace ResourceDetail {
type ClickPointMode = "point" | "box";
}
}

shared文件夹

1
2
3
4
5
6
7
|--src
|-- shared
|-- enums
|-- auth.ts
|-- step.ts
|-- mappings
|--auth.ts

此外,可以再src目录下创建shared文件夹,里面有两个子文件夹:enums和mappings.
enums文件存放枚举类型:
auth.ts

1
2
3
4
5
6
7
8
9
export namespace Auth {
export enum RoleName {
ADNIN = 'admin',
FINANCE = 'finance',
SALE = 'sale',
DELIVER = 'deliver',
REVIEW = 'review',
}
}

step.ts

1
2
3
4
5
6
7
8
export namespace Step {
export enum Status {
UNZIP = 1,
ANALYSE = 2,
LABEL = 3,
}
}

mappings文件存放映射类型:(通过JS的Map类型传入entries)

1
2
3
4
5
6
7
8
9
10
import { Auth } from "../enums/auth";

export const roleNameMapping = new Map<Auth.RoleName, string>([
[Auth.RoleName.ADNIN, /* */'管理员'],
[Auth.RoleName.FINANCE, /* */'财务'],
[Auth.RoleName.SALE, /* */'销售'],
[Auth.RoleName.DELIVER, /* */'交付'],
[Auth.RoleName.REVIEW, /* */'审核'],
])

apis文件夹

src下可以建一个apis文件夹
里面存放请求API,导出一个对象,对象里面用ts写好输入输出类型,具体的请求api调用,通过访问其属性来获取。
src/apis/user.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import http from "@utils/http";
import { API } from "shared-types";

const userAPI = {
login: async (
formData: Login.Form,
signal?: AbortSignal
): Promise<API.Response<User.Instance>> => {
try {
const { data } = await http.post("/v1/users/login", formData);
return {
success: true,
data,
};
} catch (e) {
console.error(e);
return {
success: false,
};
}
},


create: async (
data: User.Create.Data,
signal?: AbortSignal
): Promise<API.Response<void>> => {
try {
await http.post("/v1/users", data, { signal });
return {
success: true,
};
} catch (e) {
console.error(e);
return {
success: false,
};
}
},


};

export default userAPI;

views文件夹 | pages文件夹

定义组件名称与文件夹名称一样,index.ts导出,hook、ui文件放在统一路径,如果有封装子组件,放在内部管理(例如直接新增组件名,不需要再用一层component文件夹包裹)
image.pngimage.png

store文件夹

内部维护
image.png