jeecgboot
TS对比JS
| 维度 | JavaScript (JS) | TypeScript (TS) |
|---|---|---|
| 语言类型 | 动态类型,弱类型。变量类型在运行时才确定。 | 静态类型,强类型。编译时可检查类型,支持类型推断、接口和泛型。 |
| 语法 | ECMAScript 标准,支持最新 JS 语法(ES6+)。 | 基于 JS 超集,兼容 JS 所有语法,同时增加类型、接口、枚举、泛型、命名空间等。 |
| 类型系统 | 无类型系统,类型检查依赖运行时。 | 完整类型系统,编译器可提前发现类型错误,减少运行时 bug。 |
| 编译/执行 | 直接被浏览器或 Node.js 执行,无需编译。 | 需先编译成 JS(tsc 或 Babel),再由浏览器/Node.js 执行。 |
| IDE 支持 | 基础智能提示和自动补全,但类型推断有限。 | 强大的智能提示、自动补全、重构、类型检查,提高开发效率和可维护性。 |
| 错误检测 | 多数错误在运行时才发现。 | 编译时捕获类型错误和潜在逻辑错误,提高代码安全性。 |
| 面向对象 | 支持类、继承、ES6 模块,但类型不强制。 | 完整的类、接口、抽象类、访问修饰符(private/protected/public)支持,更适合大规模项目。 |
| 函数特性 | 支持高阶函数、回调、箭头函数、默认参数等。 | 除 JS 所有特性外,可为函数参数、返回值、this 指定类型,支持函数重载。 |
| 模块化 | ES Module、CommonJS、AMD 等标准。 | 完全兼容 JS 模块化,支持命名空间增强模块管理。 |
| 泛型支持 | 无原生泛型。 | 原生泛型支持,可实现类型安全的复用函数和类。 |
| 生态和库 | 丰富的库和框架生态,直接使用。 | 完全兼容 JS 生态,且可通过 DefinitelyTyped 获取类型声明文件(.d.ts)。 |
| 学习成本 | 低,适合初学者快速上手。 | 较高,需要理解类型系统和编译概念,但对大型项目更安全可维护。 |
| 调试 | 直接在浏览器或 Node.js 调试。 | 调试需要先编译为 JS,但 VS Code 等 IDE 可直接映射 TS 源码调试。 |
| 适用场景 | 小型项目、快速开发、脚本任务。 | 中大型项目、团队协作、需要严格类型和可维护性的系统。 |
| 社区和支持 | JS 社区庞大、资料丰富。 | TS 社区快速成长,Angular、NestJS、Vue3、React 等框架广泛采用 TS。 |
| 未来发展 | 稳定成熟,随着 ES 发展持续更新。 | 越来越受大型项目和企业青睐,TypeScript 已成为 JS 的标准补充。 |
- 参数类型写在括号里 params: LoginParams
- 返回值类型写在 defHttp.post<**T**> 的泛型参数里
- 不写返回值的显式类型 ------ TS 会自动推断为 Promise<LoginResultModel>
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
return defHttp.post<LoginResultModel>(
{
url: Api.Login,
params,
},
{
errorMessageMode: mode,
}
);
}
async login(
params: LoginParams & { captcha: string; rememberMe: boolean }, //属性合并
mode: ErrorMessageMode = 'modal'
): Promise<GetUserInfoModel | null> {
-
params: LoginParams & { captcha: string; rememberMe: boolean } ------ 用 & 做"交叉类型",在现有接口基础上临时扩字段
-
async 函数的返回值类型永远是 Promise<真实返回类型>
-
const data = await loginApi(...) ------ TS 会自动推断 data 为 LoginResultModel
export interface LoginParams {
username: string;
password: string;
captcha?: string;
checkKey?: string;
remember_me?: boolean;
} -
LoginParams ------ 你"发出去"给后端的结构
-
LoginResultModel ------ 后端"返回给你"的结构
-
两者分开写,避免"一个 model 既要当入参又当出参"的模糊语义
-
? 表示"可选字段",调用方可以不传
java
const loading = ref<boolean>(false);
const loginForm = reactive<LoginParams>({
username: '',
password: '',
captcha: '',
});
async function handleLogin() {
loading.value = true;
try {
// userStore.login 返回 Promise<GetUserInfoModel | null>
const userInfo = await userStore.login({
...loginForm, ///
captcha: formModelRef.value?.captcha || '',
rememberMe: formModelRef.value?.rememberMe || false,
});
notification.success({ message: '登录成功', description: `欢迎回来,${userInfo?.realname}` });
router.push('/home');
} catch (e) {
notification.error({ message: '登录失败', description: (e as Error).message });
} finally {
loading.value = false;
}
}
| 特性 | ref | reactive |
|---|---|---|
| 响应式对象类型 | 基本类型 / 对象 / 数组 | 对象 / 数组(深度) |
| 访问方式 | .value |
直接访问属性 |
| 深度响应 | 对象内部属性需访问 .value,否则浅响应 |
深度响应,内部属性也是响应式 |
| 使用场景 | 单个值(bool、number、string)或希望手动解包对象 | 表单对象、复杂数据结构、数组 |
java
loginForm.username = 'admin';
loginForm.password = '123456';
-
ref<boolean>(false) ------ 显式给泛型,防止 TS 推断出 never 或 undefined
-
reactive<LoginParams>({...}) ------ reactive 的类型建议写在尖括号里
-
(e as Error) ------ catch 里的变量默认是 unknown ,需要断言才能读 .message
captcha: formModelRef.value?.captcha || '',
从 formModelRef.value 中取出 captcha 的值,如果取不到或者值为空,则使用空字符串 '' 作为默认值。
|| '' 如果左边的值不存在(或为空值),就使用空字符串作为默认值。
这里的 问号 ?. 是 可选链运算符(Optional Chaining) ,作用就是安全地访问对象属性 ,避免对象为 null 或 undefined 时报错。
formModelRef.value == null ? undefined : formModelRef.value.captcha
- 如果
formModelRef.value是null或undefined,访问.captcha不会报错,而是返回undefined。 - 如果
formModelRef.value有值,则返回它的captcha属性。
java
let captcha;
if (formModelRef.value?.captcha) {
captcha = formModelRef.value.captcha;
} else {
captcha = '';
}
rememberMe: formModelRef.value?.rememberMe || false,
- 尝试取
rememberMe的值 - 如果取不到(
value是 null/undefined 或rememberMe本身是 falsy),就使用默认值false
泛型 <T> :你最常打交道的东西
泛型 <T> 可以理解为"类型的参数"。就像函数的参数是"值的参数":
java
// 调用处
const result = await defHttp.post<LoginResultModel>({
url: '/login',
params,
});
post<T = any>(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<T>
实际
post(
config,
options
): Promise<LoginResultModel>
return this.request<LoginResultModel>(...)
读这段代码的方式 :从调用方反着往回看 ------ "你在调用方写了 <LoginResultModel> ,这个 T 就顺着 defHttp.post<T> → request<T> → Promise<T> 一路贯穿到底"。
例如:
interface LoginResultModel {
token: string;
userId: number;
}
那么:
const result = await defHttp.post<LoginResultModel>(...)
IDE立刻知道:
result.token
result.userId
有提示。
而:
result.username
会直接报错:
Property 'username' does not exist on type 'LoginResultModel'
这就是泛型最大的价值:
src/api/model/baseModel.ts 里有两个你会反复复制的泛型模板:
java
// 所有"分页查询"都可以用这个作为基础参数
export interface BasicPageParams {
page: number;
pageSize: number;
}
// 所有"分页返回"都应该是这种结构:
// items 是数据项数组,total 是总数
export interface BasicFetchResult<T> {
items: T[];
total: number;
}
java
// src/api/sys/model/departModel.ts
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel';
export interface DepartItem {
id: string;
departName: string;
orgCode: string;
createTime: string;
}
export type DepartListParams = BasicPageParams & { departName?: string };
// & 是 交叉类型(Intersection Type)
表示把 BasicPageParams 类型和 { departName?: string } 类型 合并
结果类型拥有两边的所有属性
//? 表示 可选属性
可以有,也可以没有
类型是 string
export type DepartListResult = BasicFetchResult<DepartItem>;
// ↑ 等价于:{ items: DepartItem[]; total: number }
| 使用场景 | 推荐 | 原因 |
|---|---|---|
| 描述对象结构(API返回、表单数据) | interface |
结构语义明确,支持 extends,适合对象建模 |
| 类型别名(简单替换) | type |
更轻量,用于起别名更直观 |
| 联合类型 / 交叉类型 | type |
支持 `A |
| 函数类型定义 | type |
写法更简洁 (a: number) => string |
| class 实现约束 | interface |
implements 只能接 interface |
| 复杂类型组合 | type |
可组合、可嵌套,表达能力更强 |
- interface:更偏"对象结构设计"
- type:更偏"类型运算和组合"
- 写 对象结构 → interface
- 做 组合/运算 → type
- 不确定 → 默认 interface(更安全)
| 特性 | interface | type |
|---|---|---|
| 可重复声明 | ✅ 支持合并 | ❌ 不支持 |
| 扩展已有类型 | ✅ extends 或合并 |
✅ 交叉类型 & |
| 适合场景 | 库设计、模块扩展、API对象建模 | 联合类型、复杂类型组合、函数类型 |
java
type User = {
name: string;
};
type User = {
age: number;
};
// ❌ 会报错:Cannot redeclare block-scoped variable 'User'.
type User = { name: string };
type UserWithAge = User & { age: number };
const u: UserWithAge = { name: 'Alice', age: 18 };
在 JeecgBoot 里,最典型的是 "API model 用 interface,组合/别名用 type":
java
// 描述对象 → interface
export interface LoginParams {
username: string;
password: string;
}
// 描述"一个较长的类型的别名" → type
export type ID = string | number;
export type DepartListParams = BasicPageParams & { departName?: string };
// ↑ 交叉类型只能用 type
interface:专门描述"对象结构"
type:做"类型组合 / 运算"
-
给一个复杂或常用类型起别名
-
本质是"类型重命名 + 联合类型"
export type ID = string | number;
export type DepartListParams = BasicPageParams & { departName?: string };
👉 把两个类型"合并成一个新类型"
| 工具 | 本质 |
|---|---|
| interface | "定义一个对象长什么样" |
| type | "拼装 / 组合 / 变换类型" |
artial<T> ------ 把 T 的所有字段变成可选
源码位置: src/utils/http/axios/index.ts (createAxios 的签名)
java
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(deepMerge({ transform, requestOptions, ... }, opt || {}));
}
-
CreateAxiosOptions 有一堆字段( timeout 、 headers 、 transform 、 requestOptions ...)
-
用 Partial<CreateAxiosOptions> 表示"传进来的 opt 可以只包含其中一部分字段"
-
里面再 deepMerge(默认, opt) 把缺的字段用默认值补全
Pick<T, K> ------ 从 T 中挑出某些字段
场景:一个 User 有 10 个字段,但某个接口只需要 id 和 username 。
java
interface User {
id: string;
username: string;
email: string;
realname: string;
avatar: string;
}
type LoginUser = Pick<User, 'id' | 'username'>;
// 等价于:{ id: string; username: string }
Omit<T, K> ------ 从 T 中去掉某些字段
场景:"编辑接口"的参数和"新增接口"很像,只是多了一个 id 。
java
interface DepartAddParams {
departName: string;
orgCode: string;
parentId?: string;
}
// 编辑比新增多了一个 id 必填
type DepartEditParams = DepartAddParams & { id: string };
// 或者反过来:从"完整版"里拿掉 id
type DepartAddParams2 = Omit<DepartFullParams, 'id'>;
Record<string, T> ------ 描述一个"键值对"对象
场景: useModal / useDrawer 的 attrs 传参,或动态表单配置。
java
// 一个字典:key 是字符串,value 是任意值
const dict: Record<string, any> = {
name: 'Jeecg',
version: 3,
};
// 更具体的约束:某些固定 key
type UserFieldMap = Record<'username' | 'password', FormFieldConfig>;
何时允许 any ?何时该封掉?
动态 JSON 对象 (后端返回的 userInfo 就是典型的"结构没定死的东西")
java
export interface LoginResultModel {
userId: string | number;
token: string;
role: RoleInfo;
userInfo?: any; // ← 后端 userInfo 的字段会变动,标 any 合理
}
-
封装层的顶层 ( defHttp.get<any>(...) ------ 你自己调用的时候会把 any 替换成具体类型,所以底层留 any 没大碍)
-
第三方库不提供 d.ts ------ 临时声明 declare module 'foo'; 或 const lib: any = require('bar')
应该避免 any 的场景(3 个)
-
业务数据模型 ( LoginParams.username 写成 any ,那和没写 TS 没区别)
-
reactive / ref 状态 ( const loginForm = reactive<any>({}) ------ 你会失去所有编辑器提示)
-
函数返回值 ( async function login(): Promise<any> ------ 调用方无法知道拿到的是什么)
最佳实践 : any 只在"确实无法确定结构"的地方用,其他地方都换成具体类型或泛型。你项目里的 LoginParams 、 LoginResultModel 就是好例子。
一个完整的"从零到页面"的 TS 套路
Step 1. 写 model(数据契约)
java
// src/api/sys/model/departModel.ts
export interface DepartItem {
id: string;
departName: string;
orgCode: string;
createTime: string;
}
export type DepartListParams = BasicPageParams & { departName?: string };
export type DepartListResult = BasicFetchResult<DepartItem>;
export interface DepartSaveParams {
id?: string;
departName: string;
orgCode?: string;
parentId?: string;
}
Step 2. 写 API(带泛型返回值)
java
// src/api/sys/depart.ts
import { defHttp } from '/@/utils/http/axios';
import { DepartListParams, DepartListResult, DepartItem, DepartSaveParams } from './model/departModel';
enum Api {
LIST = '/sys/sysDepart/list',
ADD = '/sys/sysDepart/add',
EDIT = '/sys/sysDepart/edit',
DELETE = '/sys/sysDepart/delete',
}
export const departListApi = (params: DepartListParams) =>
defHttp.get<DepartListResult>({ url: Api.LIST, params });
export const departAddApi = (params: DepartSaveParams) =>
defHttp.post({ url: Api.ADD, params });
export const departEditApi = (params: DepartSaveParams) =>
defHttp.put({ url: Api.EDIT, params });
export const departDeleteApi = (id: string) =>
defHttp.delete({ url: Api.DELETE, params: { id } });
Step 3. 写 store(把 TS 类型推到状态层)
java
// src/store/modules/depart.ts
// 定义并导出一个 Pinia store 模块
// useDepartStore 是一个函数,调用它可以在组件中拿到 store 实例
export const useDepartStore = defineStore({
// store 的唯一 ID,用于区分不同模块
id: 'depart',
// state 是一个函数,返回当前模块的响应式数据对象
state: () => ({
// 当前选中的部门,初始值为 null
// TS 类型断言:可能是 DepartItem 类型或者 null
currentDepart: null as DepartItem | null,
// 部门列表,初始值为空数组
// TS 类型断言:数组中每一项都是 DepartItem 类型
departList: [] as DepartItem[],
}),
// actions 用来定义方法,可以同步或异步操作 state
actions: {
// 异步加载部门列表
// params: DepartListParams 类型,通常包含分页或搜索条件
async loadList(params: DepartListParams) {
// 调用 API 获取部门列表
// departListApi(params) 返回一个 Promise<DepartItem[]>
// await 等待异步结果
const list = await departListApi(params);
// 把获取到的部门列表保存到 store 的 state
// this.departList 直接访问 store 中的 departList
this.departList = list;
},
},
});
departList 的类型是:DepartItem\[\]
-
空数组
[]本身 TS 会推断类型为any[] -
如果不加类型断言,写法如下会报错:
this.departList = await departListApi(params); // TS 可能报类型不匹配
加上 as DepartItem[] 后:
departList明确类型,TS 能检查赋值正确性- 保证数组中元素都是
DepartItem,避免后续访问报错


| 写法 | 面向对象 | 作用 | TS 类型检查 |
|---|---|---|---|
departList: [] as DepartItem[] |
变量/字段 | 明确字段类型 | 检查字段赋值类型 |
params: DepartListParams |
函数参数 | 指定函数调用接口 | 检查调用者传参类型 |
Step 4. 写页面(享受自动提示)
java
// src/views/system/depart/index.vue
const { departList, total } = await departListApi({
page: 1,
pageSize: 10,
departName: formState.departName, // ← 这里敲 departName. 会出字段提示
});
写完 Step 1 → Step 4 之后, 只要后端的响应结构有变动 ,你改一下 departModel.ts ,所有用到它的地方 TS 都会帮你标出来。这就是 TS 的核心价值。